profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

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