profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

main default branch 418 files Expires Sep 13, 2026, 9:06 AM
PollsController.cs 8,999 bytes
1 using System.Security.Claims;
2 using Asp.Versioning;
3 using MediatR;
4 using Microsoft.AspNetCore.Authentication.JwtBearer;
5 using Microsoft.AspNetCore.Authorization;
6 using Microsoft.AspNetCore.Mvc;
7 using SplitApp.Modules.Trips.Api.Dto.v1;
8 using SplitApp.Modules.Trips.Application.Contracts;
9 using SplitApp.Modules.Trips.Domain.Entities;
10 using SplitApp.Modules.Trips.Domain.Enums;
11
12 namespace SplitApp.Modules.Trips.Api.Controllers;
13
14 [ApiVersion("1.0")]
15 [ApiController]
16 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
17 [Route("api/v{version:apiVersion}/[controller]")]
18 public class PollsController : ControllerBase
19 {
20 private readonly ITripsUnitOfWork _uow;
21
22 public PollsController(ITripsUnitOfWork uow)
23 {
24 _uow = uow;
25 }
26
27 [HttpGet("trip/{tripId:guid}")]
28 public async Task<ActionResult<List<PollDto>>> GetForTrip(Guid tripId)
29 {
30 var userId = CurrentUserId();
31 if (userId == null) return Unauthorized();
32
33 if (!await IsParticipantAsync(tripId, userId.Value)) return Forbid();
34
35 var allPolls = await _uow.Polls.GetAllAsync();
36 var polls = allPolls.Where(p => p.TripId == tripId).ToList();
37 var allOptions = await _uow.PollOptions.GetAllAsync();
38 var allVotes = await _uow.PollVotes.GetAllAsync();
39
40 return Ok(polls.Select(p => MapPoll(p, allOptions, allVotes, userId.Value)).ToList());
41 }
42
43 [HttpGet("{id:guid}")]
44 public async Task<ActionResult<PollDto>> Get(Guid id)
45 {
46 var userId = CurrentUserId();
47 if (userId == null) return Unauthorized();
48
49 var poll = await _uow.Polls.GetByIdAsync(id);
50 if (poll == null) return NotFound();
51 if (!await IsParticipantAsync(poll.TripId, userId.Value)) return NotFound();
52
53 var allOptions = await _uow.PollOptions.GetAllAsync();
54 var allVotes = await _uow.PollVotes.GetAllAsync();
55 return Ok(MapPoll(poll, allOptions, allVotes, userId.Value));
56 }
57
58 [HttpPost]
59 public async Task<ActionResult<PollDto>> Create([FromBody] PollCreateDto dto)
60 {
61 var userId = CurrentUserId();
62 if (userId == null) return Unauthorized();
63
64 if (!await IsParticipantAsync(dto.TripId, userId.Value)) return Forbid();
65
66 if (dto.Options == null || dto.Options.Count < 2)
67 return BadRequest("Poll must have at least two options.");
68
69 var poll = new TripPoll
70 {
71 TripId = dto.TripId,
72 CreatedByUserId = userId.Value,
73 Question = dto.Question,
74 AllowMultipleVotes = dto.AllowMultipleVotes,
75 IsAnonymous = dto.IsAnonymous,
76 };
77 _uow.Polls.Add(poll);
78
79 var order = 0;
80 foreach (var optionText in dto.Options)
81 {
82 _uow.PollOptions.Add(new TripPollOption
83 {
84 PollId = poll.Id,
85 Text = optionText,
86 DisplayOrder = order++,
87 });
88 }
89
90 await _uow.SaveChangesAsync();
91
92 var allOptions = await _uow.PollOptions.GetAllAsync();
93 var allVotes = await _uow.PollVotes.GetAllAsync();
94 var dtoResult = MapPoll(poll, allOptions, allVotes, userId.Value);
95 return CreatedAtAction(nameof(Get), new { id = poll.Id }, dtoResult);
96 }
97
98 [HttpPost("{id:guid}/vote")]
99 public async Task<IActionResult> Vote(Guid id, [FromBody] PollVoteRequest request)
100 {
101 var userId = CurrentUserId();
102 if (userId == null) return Unauthorized();
103
104 var poll = await _uow.Polls.GetByIdAsync(id);
105 if (poll == null) return NotFound();
106
107 if (poll.ClosedAt != null) return BadRequest("Poll is closed.");
108
109 if (!await IsParticipantAsync(poll.TripId, userId.Value)) return Forbid();
110
111 var option = await _uow.PollOptions.GetByIdAsync(request.OptionId);
112 if (option == null || option.PollId != poll.Id) return BadRequest("Invalid option.");
113
114 var allVotes = await _uow.PollVotes.GetAllAsync();
115 var pollOptionIds = (await _uow.PollOptions.GetAllAsync())
116 .Where(o => o.PollId == poll.Id)
117 .Select(o => o.Id)
118 .ToHashSet();
119
120 var existingVotesForUser = allVotes
121 .Where(v => v.UserId == userId.Value && pollOptionIds.Contains(v.PollOptionId))
122 .ToList();
123
124 if (!poll.AllowMultipleVotes)
125 {
126 foreach (var v in existingVotesForUser)
127 {
128 await _uow.PollVotes.RemoveAsync(v.Id);
129 }
130 }
131 else if (existingVotesForUser.Any(v => v.PollOptionId == request.OptionId))
132 {
133 // Toggle off if same option voted again under multi-vote
134 var existing = existingVotesForUser.First(v => v.PollOptionId == request.OptionId);
135 await _uow.PollVotes.RemoveAsync(existing.Id);
136 await _uow.SaveChangesAsync();
137 return Ok();
138 }
139
140 _uow.PollVotes.Add(new TripPollVote
141 {
142 PollOptionId = request.OptionId,
143 UserId = userId.Value,
144 });
145
146 await _uow.SaveChangesAsync();
147 return Ok();
148 }
149
150 [HttpPost("{id:guid}/close")]
151 public async Task<IActionResult> Close(Guid id)
152 {
153 var userId = CurrentUserId();
154 if (userId == null) return Unauthorized();
155
156 var poll = await _uow.Polls.GetByIdAsync(id);
157 if (poll == null) return NotFound();
158
159 var canClose = poll.CreatedByUserId == userId.Value
160 || await IsOrganizerAsync(poll.TripId, userId.Value);
161 if (!canClose) return Forbid();
162
163 poll.ClosedAt = DateTime.UtcNow;
164 _uow.Polls.Update(poll);
165 await _uow.SaveChangesAsync();
166 return Ok();
167 }
168
169 [HttpDelete("{id:guid}")]
170 public async Task<IActionResult> Delete(Guid id)
171 {
172 var userId = CurrentUserId();
173 if (userId == null) return Unauthorized();
174
175 var poll = await _uow.Polls.GetByIdAsync(id);
176 if (poll == null) return NotFound();
177
178 var canDelete = poll.CreatedByUserId == userId.Value
179 || await IsOrganizerAsync(poll.TripId, userId.Value);
180 if (!canDelete) return Forbid();
181
182 var allOptions = await _uow.PollOptions.GetAllAsync();
183 var allVotes = await _uow.PollVotes.GetAllAsync();
184 var optionIds = allOptions.Where(o => o.PollId == poll.Id).Select(o => o.Id).ToHashSet();
185
186 foreach (var v in allVotes.Where(v => optionIds.Contains(v.PollOptionId)))
187 {
188 await _uow.PollVotes.RemoveAsync(v.Id);
189 }
190 foreach (var oid in optionIds)
191 {
192 await _uow.PollOptions.RemoveAsync(oid);
193 }
194 await _uow.Polls.RemoveAsync(id);
195 await _uow.SaveChangesAsync();
196 return NoContent();
197 }
198
199 private Guid? CurrentUserId()
200 {
201 var raw = User.FindFirstValue(ClaimTypes.NameIdentifier);
202 return Guid.TryParse(raw, out var id) ? id : null;
203 }
204
205 private async Task<bool> IsParticipantAsync(Guid tripId, Guid userId)
206 {
207 var trip = await _uow.Trips.GetByIdAsync(tripId);
208 if (trip == null) return false;
209 if (trip.CreatedById == userId) return true;
210 var all = await _uow.Participants.GetAllAsync();
211 return all.Any(p => p.TripId == tripId && p.UserId == userId && p.IsActive);
212 }
213
214 private async Task<bool> IsOrganizerAsync(Guid tripId, Guid userId)
215 {
216 var trip = await _uow.Trips.GetByIdAsync(tripId);
217 if (trip == null) return false;
218 if (trip.CreatedById == userId) return true;
219 var all = await _uow.Participants.GetAllAsync();
220 return all.Any(p => p.TripId == tripId
221 && p.UserId == userId
222 && p.IsActive
223 && p.Role == EParticipantRole.Organizer);
224 }
225
226 private static PollDto MapPoll(
227 TripPoll poll,
228 IEnumerable<TripPollOption> allOptions,
229 IEnumerable<TripPollVote> allVotes,
230 Guid currentUserId)
231 {
232 var options = allOptions.Where(o => o.PollId == poll.Id).OrderBy(o => o.DisplayOrder).ToList();
233 var optionIds = options.Select(o => o.Id).ToHashSet();
234 var votes = allVotes.Where(v => optionIds.Contains(v.PollOptionId)).ToList();
235
236 return new PollDto
237 {
238 Id = poll.Id,
239 TripId = poll.TripId,
240 CreatedByUserId = poll.CreatedByUserId,
241 Question = poll.Question,
242 AllowMultipleVotes = poll.AllowMultipleVotes,
243 IsAnonymous = poll.IsAnonymous,
244 ClosedAt = poll.ClosedAt,
245 Options = options.Select(o => new PollOptionDto
246 {
247 Id = o.Id,
248 Text = o.Text,
249 DisplayOrder = o.DisplayOrder,
250 VoteCount = votes.Count(v => v.PollOptionId == o.Id),
251 VotedByCurrentUser = votes.Any(v => v.PollOptionId == o.Id && v.UserId == currentUserId),
252 }).ToList(),
253 };
254 }
255 }
256