profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
ExpensesController.cs 9,999 bytes
1 using SplitApp.WebApp.Application.DTO;
2 using SplitApp.WebApp.Application.Services;
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.Modules.Users.Domain.Entities;
9 using Microsoft.AspNetCore.Authorization;
10 using Microsoft.AspNetCore.Identity;
11 using Microsoft.AspNetCore.Mvc;
12 using Microsoft.AspNetCore.Mvc.Rendering;
13
14 namespace SplitApp.WebApp.Controllers;
15
16 [Authorize]
17 public class ExpensesController : Controller
18 {
19 private readonly IExpenseService _expenseService;
20 private readonly ITripService _tripService;
21 private readonly IBudgetCategoryService _budgetCategoryService;
22 private readonly UserManager<AppUser> _userManager;
23
24 public ExpensesController(
25 IExpenseService expenseService,
26 ITripService tripService,
27 IBudgetCategoryService budgetCategoryService,
28 UserManager<AppUser> userManager)
29 {
30 _expenseService = expenseService;
31 _tripService = tripService;
32 _budgetCategoryService = budgetCategoryService;
33 _userManager = userManager;
34 }
35
36 private Guid GetUserId() => Guid.Parse(_userManager.GetUserId(User)!);
37
38 // GET: Expenses?tripId=xxx
39 public async Task<IActionResult> Index(Guid tripId)
40 {
41 var userId = GetUserId();
42 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
43
44 var trip = await _tripService.GetByIdWithDetailsAsync(tripId, userId);
45 if (trip == null) return NotFound();
46
47 var expenses = await _expenseService.GetByTripIdAsync(tripId, userId);
48
49 var model = new ExpensesIndexViewModel
50 {
51 TripId = tripId,
52 TripName = trip.Name,
53 DefaultCurrencyCode = trip.DefaultCurrency?.Code ?? "EUR",
54 CurrencySymbol = trip.DefaultCurrency?.Symbol ?? "\u20ac",
55 TripStatus = trip.Status.ToString(),
56 Expenses = expenses
57 };
58
59 return View(model);
60 }
61
62 // GET: Expenses/Create?tripId=xxx
63 public async Task<IActionResult> Create(Guid tripId)
64 {
65 var userId = GetUserId();
66 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
67
68 var trip = await _tripService.GetByIdAsync(tripId, userId);
69 if (trip != null && trip.Status != ETripStatus.Active)
70 return RedirectToAction(nameof(Index), new { tripId });
71
72 await PopulateDropdowns(tripId);
73 ViewData["TripId"] = tripId;
74
75 var expense = new ExpenseBllDto
76 {
77 TripId = tripId,
78 PaidByUserId = userId,
79 ExpenseDate = DateTime.UtcNow,
80 SplitMethod = ESplitMethod.EqualAll
81 };
82
83 return View(expense);
84 }
85
86 // POST: Expenses/Create
87 [HttpPost]
88 [ValidateAntiForgeryToken]
89 public async Task<IActionResult> Create(ExpenseBllDto expense, Guid[] selectedParticipants, decimal[] splitAmounts, decimal[] splitPercentages)
90 {
91 var userId = GetUserId();
92 if (!await _tripService.IsParticipantAsync(expense.TripId, userId)) return Forbid();
93
94 if (ModelState.IsValid)
95 {
96 await _expenseService.CreateExpenseWithSplitsAsync(expense, selectedParticipants, splitAmounts, splitPercentages);
97 return RedirectToAction(nameof(Index), new { tripId = expense.TripId });
98 }
99
100 await PopulateDropdowns(expense.TripId, expense.BudgetCategoryId, expense.CurrencyId);
101 ViewData["TripId"] = expense.TripId;
102 return View(expense);
103 }
104
105 // GET: Expenses/Edit/5
106 public async Task<IActionResult> Edit(Guid id)
107 {
108 var userId = GetUserId();
109
110 var expense = await _expenseService.GetRawByIdAsync(id);
111 if (expense == null) return NotFound();
112
113 if (!await _expenseService.CanEditExpenseAsync(expense, userId)) return Forbid();
114
115 var trip = await _tripService.GetRawByIdAsync(expense.TripId);
116 if (trip != null && trip.Status != ETripStatus.Active)
117 return RedirectToAction(nameof(Index), new { tripId = expense.TripId });
118
119 await PopulateDropdowns(expense.TripId, expense.BudgetCategoryId, expense.CurrencyId);
120 ViewData["TripId"] = expense.TripId;
121
122 return View(expense);
123 }
124
125 // POST: Expenses/Edit/5
126 [HttpPost]
127 [ValidateAntiForgeryToken]
128 public async Task<IActionResult> Edit(Guid id, ExpenseBllDto expense)
129 {
130 if (id != expense.Id) return NotFound();
131
132 var userId = GetUserId();
133
134 if (ModelState.IsValid)
135 {
136 var result = await _expenseService.UpdateExpenseAsync(id, expense, userId);
137 if (!result.success)
138 {
139 return result.errorCode switch
140 {
141 "notfound" => NotFound(),
142 "forbidden" => Forbid(),
143 "badstatus" => RedirectToAction(nameof(Index), new { tripId = expense.TripId }),
144 _ => NotFound()
145 };
146 }
147 // Need to get tripId from existing since it's not in incoming after success
148 var updated = await _expenseService.GetRawByIdAsync(id);
149 return RedirectToAction(nameof(Index), new { tripId = updated?.TripId ?? expense.TripId });
150 }
151
152 var existingEntity = await _expenseService.GetRawByIdAsync(id);
153 if (existingEntity == null) return NotFound();
154
155 await PopulateDropdowns(existingEntity.TripId, expense.BudgetCategoryId, expense.CurrencyId);
156 ViewData["TripId"] = existingEntity.TripId;
157 return View(expense);
158 }
159
160 // GET: Expenses/Delete/5
161 public async Task<IActionResult> Delete(Guid id)
162 {
163 var userId = GetUserId();
164
165 var expense = await _expenseService.GetByIdWithDetailsAsync(id, userId);
166 if (expense == null)
167 {
168 // Either NotFound or not a participant
169 var raw = await _expenseService.GetRawByIdAsync(id);
170 if (raw == null) return NotFound();
171 return Forbid();
172 }
173
174 if (!await _expenseService.CanEditExpenseAsync(expense, userId)) return Forbid();
175
176 var trip = await _tripService.GetRawByIdAsync(expense.TripId);
177 if (trip != null && trip.Status != ETripStatus.Active)
178 return RedirectToAction(nameof(Index), new { tripId = expense.TripId });
179
180 ViewData["TripId"] = expense.TripId;
181 return View(expense);
182 }
183
184 // POST: Expenses/Delete/5
185 [HttpPost, ActionName("Delete")]
186 [ValidateAntiForgeryToken]
187 public async Task<IActionResult> DeleteConfirmed(Guid id)
188 {
189 var userId = GetUserId();
190
191 var expense = await _expenseService.GetRawByIdAsync(id);
192 if (expense == null) return NotFound();
193
194 if (!await _expenseService.CanEditExpenseAsync(expense, userId)) return Forbid();
195
196 var trip = await _tripService.GetRawByIdAsync(expense.TripId);
197 if (trip != null && trip.Status != ETripStatus.Active)
198 return RedirectToAction(nameof(Index), new { tripId = expense.TripId });
199
200 var tripId = expense.TripId;
201
202 await _expenseService.DeleteExpenseWithSplitsAsync(id);
203
204 return RedirectToAction(nameof(Index), new { tripId });
205 }
206
207 private async Task PopulateDropdowns(Guid tripId, Guid? selectedCategoryId = null, Guid? selectedCurrencyId = null)
208 {
209 var categories = await _budgetCategoryService.GetByTripIdRawAsync(tripId);
210 ViewData["BudgetCategoryId"] = new SelectList(categories, "Id", "Name", selectedCategoryId);
211
212 var currencies = await _tripService.GetAllCurrenciesAsync();
213 ViewData["CurrencyId"] = new SelectList(currencies, "Id", "Code", selectedCurrencyId);
214
215 ViewData["SplitMethods"] = new SelectList(
216 Enum.GetValues<ESplitMethod>().Select(e => new { Value = (int)e, Text = e.ToString() }),
217 "Value", "Text");
218
219 var userId = GetUserId();
220 var participants = await _tripService.GetParticipantsAsync(tripId, userId);
221
222 ViewData["Participants"] = participants;
223 ViewData["PaidByUserId"] = new SelectList(
224 participants.Select(p => new
225 {
226 Value = p.UserId,
227 Text = $"{p.User!.FirstName} {p.User.LastName}"
228 }),
229 "Value", "Text", userId);
230
231 // Load split presets for this trip
232 var presets = await _expenseService.GetSplitPresetsByTripAsync(tripId, userId);
233 ViewData["SplitPresets"] = presets;
234 }
235
236 // POST: Expenses/SavePreset
237 [HttpPost]
238 [ValidateAntiForgeryToken]
239 public async Task<IActionResult> SavePreset(Guid tripId, string presetName, int splitMethod, Guid[] selectedParticipants, decimal[] splitPercentages)
240 {
241 var userId = GetUserId();
242 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
243
244 await _expenseService.SavePresetAsync(tripId, presetName, (ESplitMethod)splitMethod, selectedParticipants, splitPercentages, userId);
245 return RedirectToAction(nameof(Create), new { tripId });
246 }
247
248 // POST: Expenses/DeletePreset
249 [HttpPost]
250 [ValidateAntiForgeryToken]
251 public async Task<IActionResult> DeletePreset(Guid id, Guid tripId)
252 {
253 var userId = GetUserId();
254 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
255
256 await _expenseService.DeletePresetAsync(id, tripId, userId);
257
258 return RedirectToAction(nameof(Create), new { tripId });
259 }
260 }
261
262 public class ExpensesIndexViewModel
263 {
264 public Guid TripId { get; set; }
265 public string TripName { get; set; } = default!;
266 public string DefaultCurrencyCode { get; set; } = default!;
267 public string CurrencySymbol { get; set; } = default!;
268 public string TripStatus { get; set; } = default!;
269 public List<ExpenseBllDto> Expenses { get; set; } = new();
270 }
271