profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
SettlementController.cs 6,629 bytes
1 using SplitApp.WebApp.Application.Services;
2 using SplitApp.Modules.Trips.Domain.Entities;
3 using SplitApp.Modules.Trips.Domain.Enums;
4 using SplitApp.Modules.Expenses.Domain.Entities;
5 using SplitApp.Modules.Expenses.Domain.Enums;
6 using SplitApp.Modules.Users.Domain.Entities;
7 using SplitApp.Modules.Users.Domain.Entities;
8 using Microsoft.AspNetCore.Authorization;
9 using Microsoft.AspNetCore.Identity;
10 using Microsoft.AspNetCore.Mvc;
11
12 namespace SplitApp.WebApp.Controllers;
13
14 [Authorize]
15 public class SettlementController : Controller
16 {
17 private readonly ITripService _tripService;
18 private readonly ISettlementService _settlementService;
19 private readonly UserManager<AppUser> _userManager;
20
21 public SettlementController(
22 ITripService tripService,
23 ISettlementService settlementService,
24 UserManager<AppUser> userManager)
25 {
26 _tripService = tripService;
27 _settlementService = settlementService;
28 _userManager = userManager;
29 }
30
31 private Guid GetUserId() => Guid.Parse(_userManager.GetUserId(User)!);
32
33 // GET: Settlement?tripId=xxx
34 public async Task<IActionResult> Index(Guid tripId)
35 {
36 var userId = GetUserId();
37 if (!await _tripService.IsParticipantAsync(tripId, userId)) return Forbid();
38
39 var trip = await _tripService.GetByIdWithDetailsAsync(tripId, userId);
40 if (trip == null) return NotFound();
41
42 // Calculate balances via service
43 var balanceEntries = await _settlementService.CalculateBalancesAsync(tripId);
44
45 // Map to view model
46 var balances = balanceEntries.Select(b => new SettlementBalanceViewModel
47 {
48 UserId = b.UserId,
49 UserName = b.UserName,
50 TotalPaid = b.TotalPaid,
51 TotalOwed = b.TotalOwed
52 }).OrderByDescending(b => b.NetBalance).ToList();
53
54 // Get existing settlement plan (only exists after trip is finalized)
55 var latestPlan = await _settlementService.GetLatestPlanRawAsync(tripId);
56
57 // Preview payments when trip is active (not saved to DB)
58 var previewPayments = trip.Status == ETripStatus.Active
59 ? _settlementService.PreviewSettlement(balanceEntries)
60 : new List<PreviewPayment>();
61
62 var model = new SettlementIndexViewModel
63 {
64 TripId = tripId,
65 TripName = trip.Name,
66 CurrencySymbol = trip.DefaultCurrency?.Symbol ?? "$",
67 TripStatus = trip.Status.ToString(),
68 IsOrganizer = await _tripService.IsOrganizerAsync(tripId, userId),
69 CurrentUserId = userId,
70 Balances = balances,
71 LatestPlan = latestPlan,
72 PreviewPayments = previewPayments
73 };
74
75 return View(model);
76 }
77
78 // POST: Settlement/Finalize
79 [HttpPost]
80 [ValidateAntiForgeryToken]
81 public async Task<IActionResult> Finalize(Guid tripId)
82 {
83 var userId = GetUserId();
84 var (ok, errorCode) = await _tripService.FinalizeTripAsync(tripId, userId);
85 if (!ok)
86 {
87 return errorCode switch
88 {
89 "forbidden" => Forbid(),
90 "notfound" => NotFound(),
91 "badstatus" => BadRequest(),
92 _ => NotFound()
93 };
94 }
95
96 return RedirectToAction(nameof(Index), new { tripId });
97 }
98
99 // POST: Settlement/Reopen
100 [HttpPost]
101 [ValidateAntiForgeryToken]
102 public async Task<IActionResult> Reopen(Guid tripId)
103 {
104 var userId = GetUserId();
105 var (ok, errorCode) = await _tripService.ReopenTripAsync(tripId, userId);
106 if (!ok)
107 {
108 return errorCode switch
109 {
110 "forbidden" => Forbid(),
111 "notfound" => NotFound(),
112 "badstatus" => RedirectToAction(nameof(Index), new { tripId }),
113 "payments-confirmed" => RedirectToAction(nameof(Index), new { tripId }),
114 _ => NotFound()
115 };
116 }
117
118 return RedirectToAction(nameof(Index), new { tripId });
119 }
120
121 // POST: Settlement/MarkPaid/5
122 [HttpPost]
123 [ValidateAntiForgeryToken]
124 public async Task<IActionResult> MarkPaid(Guid paymentId)
125 {
126 var userId = GetUserId();
127 var payment = await _settlementService.GetPaymentByIdAsync(paymentId);
128 if (payment == null) return NotFound();
129
130 var plan = await _settlementService.GetPlanByIdAsync(payment.SettlementPlanId);
131 if (plan == null) return NotFound();
132
133 var (ok, errorCode) = await _settlementService.MarkPaidGuardedAsync(paymentId, userId);
134 if (!ok)
135 {
136 return errorCode switch
137 {
138 "forbidden" => Forbid(),
139 "notfound" => NotFound(),
140 _ => NotFound()
141 };
142 }
143
144 return RedirectToAction(nameof(Index), new { tripId = plan.TripId });
145 }
146
147 // POST: Settlement/ConfirmReceipt/5
148 [HttpPost]
149 [ValidateAntiForgeryToken]
150 public async Task<IActionResult> ConfirmReceipt(Guid paymentId)
151 {
152 var userId = GetUserId();
153 var payment = await _settlementService.GetPaymentByIdAsync(paymentId);
154 if (payment == null) return NotFound();
155
156 var plan = await _settlementService.GetPlanByIdAsync(payment.SettlementPlanId);
157 if (plan == null) return NotFound();
158
159 var (ok, errorCode) = await _settlementService.ConfirmPaymentGuardedAsync(paymentId, userId);
160 if (!ok)
161 {
162 return errorCode switch
163 {
164 "forbidden" => Forbid(),
165 "notfound" => NotFound(),
166 _ => NotFound()
167 };
168 }
169
170 return RedirectToAction(nameof(Index), new { tripId = plan.TripId });
171 }
172 }
173
174 public class SettlementIndexViewModel
175 {
176 public Guid TripId { get; set; }
177 public string TripName { get; set; } = default!;
178 public string CurrencySymbol { get; set; } = default!;
179 public string TripStatus { get; set; } = default!;
180 public bool IsOrganizer { get; set; }
181 public Guid CurrentUserId { get; set; }
182 public List<SettlementBalanceViewModel> Balances { get; set; } = new();
183 public SplitApp.WebApp.Application.DTO.SettlementPlanBllDto? LatestPlan { get; set; }
184 public List<PreviewPayment> PreviewPayments { get; set; } = new();
185 }
186
187 public class SettlementBalanceViewModel
188 {
189 public Guid UserId { get; set; }
190 public string UserName { get; set; } = default!;
191 public decimal TotalPaid { get; set; }
192 public decimal TotalOwed { get; set; }
193 public decimal NetBalance => TotalPaid - TotalOwed;
194 }
195