profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
PollService.cs 6,753 bytes
1 using App.BLL.DTO;
2 using App.BLL.Mappers;
3 using App.Domain;
4 using App.Domain.Contracts;
5
6 namespace App.BLL.Services;
7
8 public class PollService : IPollService
9 {
10 private readonly IAppUnitOfWork _uow;
11
12 public PollService(IAppUnitOfWork uow)
13 {
14 _uow = uow;
15 }
16
17 public async Task<TripPollBllDto> CreatePollWithOptionsAsync(TripPollBllDto poll, List<string> optionTexts)
18 {
19 var entity = PollBllDtoFactory.ToEntity(poll);
20 entity.Id = Guid.NewGuid();
21 _uow.TripPolls.Add(entity);
22
23 var optionRepo = _uow.GetRepository<TripPollOption>();
24 var order = 0;
25
26 foreach (var text in optionTexts.Where(t => !string.IsNullOrWhiteSpace(t)))
27 {
28 optionRepo.Add(new TripPollOption
29 {
30 Id = Guid.NewGuid(),
31 PollId = entity.Id,
32 Text = text,
33 DisplayOrder = order++
34 });
35 }
36
37 await _uow.SaveChangesAsync();
38
39 return PollBllDtoFactory.Create(entity);
40 }
41
42 public async Task ToggleVoteAsync(Guid pollId, Guid optionId, Guid userId)
43 {
44 var poll = await _uow.TripPolls.GetByIdWithDetailsAsync(pollId);
45 if (poll == null) return;
46
47 if (poll.ClosedAt != null) return;
48
49 var option = poll.Options?.FirstOrDefault(o => o.Id == optionId);
50 if (option == null) return;
51
52 var voteRepo = _uow.GetRepository<TripPollVote>();
53
54 if (!poll.AllowMultipleVotes)
55 {
56 if (poll.Options != null)
57 {
58 foreach (var opt in poll.Options)
59 {
60 if (opt.Votes != null)
61 {
62 foreach (var vote in opt.Votes.Where(v => v.UserId == userId).ToList())
63 {
64 await voteRepo.RemoveAsync(vote.Id);
65 }
66 }
67 }
68 }
69 }
70
71 var existingVote = option.Votes?.FirstOrDefault(v => v.UserId == userId);
72
73 if (existingVote != null)
74 {
75 await voteRepo.RemoveAsync(existingVote.Id);
76 }
77 else
78 {
79 voteRepo.Add(new TripPollVote
80 {
81 Id = Guid.NewGuid(),
82 PollOptionId = optionId,
83 UserId = userId
84 });
85 }
86
87 await _uow.SaveChangesAsync();
88 }
89
90 public async Task<List<TripPollBllDto>> GetByTripIdAsync(Guid tripId, Guid userId)
91 {
92 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId))
93 return new List<TripPollBllDto>();
94 var polls = await _uow.TripPolls.GetByTripIdAsync(tripId);
95 return PollBllDtoFactory.CreateList(polls, includeOptions: true);
96 }
97
98 public async Task<TripPollBllDto?> GetByIdAsync(Guid pollId, Guid userId)
99 {
100 var poll = await _uow.TripPolls.GetByIdAsync(pollId);
101 if (poll == null) return null;
102 if (!await _uow.TripParticipants.IsParticipantAsync(poll.TripId, userId)) return null;
103 return PollBllDtoFactory.Create(poll);
104 }
105
106 public async Task<TripPollBllDto?> GetByIdWithDetailsAsync(Guid pollId, Guid userId)
107 {
108 var poll = await _uow.TripPolls.GetByIdWithDetailsAsync(pollId);
109 if (poll == null) return null;
110 if (!await _uow.TripParticipants.IsParticipantAsync(poll.TripId, userId)) return null;
111 return PollBllDtoFactory.Create(poll, includeOptions: true);
112 }
113
114 public async Task<(TripPollBllDto? poll, string? errorCode)> CreatePollGuardedAsync(TripPollBllDto poll, List<string> optionTexts, Guid userId)
115 {
116 if (!await _uow.TripParticipants.IsParticipantAsync(poll.TripId, userId))
117 return (null, "forbidden");
118 var created = await CreatePollWithOptionsAsync(poll, optionTexts);
119 return (created, null);
120 }
121
122 public async Task<(bool success, string? errorCode)> CastVoteAsync(Guid pollId, Guid optionId, Guid userId)
123 {
124 var poll = await _uow.TripPolls.GetByIdWithDetailsAsync(pollId);
125 if (poll == null) return (false, "notfound");
126
127 if (poll.ClosedAt != null) return (false, "closed");
128
129 if (!await _uow.TripParticipants.IsParticipantAsync(poll.TripId, userId))
130 return (false, "forbidden");
131
132 var option = poll.Options?.FirstOrDefault(o => o.Id == optionId);
133 if (option == null) return (false, "invalid-option");
134
135 await ToggleVoteAsync(pollId, optionId, userId);
136 return (true, null);
137 }
138
139 public async Task<(bool success, string? errorCode)> ClosePollAsync(Guid pollId, Guid userId, bool organizerAllowed)
140 {
141 var poll = await _uow.TripPolls.GetByIdAsync(pollId);
142 if (poll == null) return (false, "notfound");
143
144 if (!await _uow.TripParticipants.IsParticipantAsync(poll.TripId, userId))
145 return (false, "forbidden");
146
147 var isCreator = poll.CreatedByUserId == userId;
148 var isOrganizer = organizerAllowed && await _uow.TripParticipants.IsOrganizerAsync(poll.TripId, userId);
149
150 if (!isCreator && !isOrganizer) return (false, "forbidden");
151
152 poll.ClosedAt = DateTime.UtcNow;
153 _uow.TripPolls.Update(poll);
154 await _uow.SaveChangesAsync();
155 return (true, null);
156 }
157
158 public async Task<(bool success, string? errorCode)> DeletePollGuardedAsync(Guid pollId, Guid userId)
159 {
160 var poll = await _uow.TripPolls.GetByIdWithDetailsAsync(pollId);
161 if (poll == null) return (false, "notfound");
162
163 var isCreator = poll.CreatedByUserId == userId;
164 var isOrganizer = await _uow.TripParticipants.IsOrganizerAsync(poll.TripId, userId);
165
166 if (!isCreator && !isOrganizer) return (false, "forbidden");
167
168 await DeletePollCascadeAsync(pollId);
169 return (true, null);
170 }
171
172 public async Task DeletePollCascadeAsync(Guid pollId)
173 {
174 var poll = await _uow.TripPolls.GetByIdWithDetailsAsync(pollId);
175 if (poll == null) return;
176
177 var voteRepo = _uow.GetRepository<TripPollVote>();
178 var optionRepo = _uow.GetRepository<TripPollOption>();
179
180 if (poll.Options != null)
181 {
182 foreach (var option in poll.Options)
183 {
184 if (option.Votes != null)
185 {
186 foreach (var vote in option.Votes.ToList())
187 {
188 await voteRepo.RemoveAsync(vote.Id);
189 }
190 }
191 }
192
193 foreach (var option in poll.Options.ToList())
194 {
195 await optionRepo.RemoveAsync(option.Id);
196 }
197 }
198
199 await _uow.TripPolls.RemoveAsync(pollId);
200
201 await _uow.SaveChangesAsync();
202 }
203 }
204