profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

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