profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
InvitationService.cs 6,853 bytes
1 using System.Security.Cryptography;
2 using App.BLL.DTO;
3 using App.BLL.Mappers;
4 using App.Domain;
5 using App.Domain.Contracts;
6
7 namespace App.BLL.Services;
8
9 public class InvitationService : IInvitationService
10 {
11 private readonly IAppUnitOfWork _uow;
12
13 public InvitationService(IAppUnitOfWork uow)
14 {
15 _uow = uow;
16 }
17
18 public async Task<TripInvitationBllDto> CreateInvitationAsync(Guid tripId, Guid userId)
19 {
20 var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
21 .Replace("+", "-").Replace("/", "_").TrimEnd('=');
22
23 var invitation = new TripInvitation
24 {
25 Id = Guid.NewGuid(),
26 TripId = tripId,
27 InvitedByUserId = userId,
28 Token = token,
29 Status = EInvitationStatus.Pending,
30 ExpiresAt = DateTime.UtcNow.AddDays(7)
31 };
32
33 _uow.TripInvitations.Add(invitation);
34 await _uow.SaveChangesAsync();
35
36 return InvitationBllDtoFactory.Create(invitation);
37 }
38
39 public async Task<bool> AcceptInvitationAsync(string token, Guid userId)
40 {
41 var invitation = await _uow.TripInvitations.GetByTokenAsync(token);
42 if (invitation == null) return false;
43
44 if (invitation.Status != EInvitationStatus.Pending || invitation.ExpiresAt < DateTime.UtcNow)
45 return false;
46
47 // Check if already a participant
48 var participants = (await _uow.TripParticipants.GetByTripIdAsync(invitation.TripId)).ToList();
49 var existingParticipant = participants.FirstOrDefault(tp => tp.UserId == userId);
50
51 var allParticipants = await _uow.TripParticipants.GetAllAsync();
52 var inactiveParticipant = allParticipants
53 .FirstOrDefault(tp => tp.TripId == invitation.TripId && tp.UserId == userId && !tp.IsActive);
54
55 if (existingParticipant != null)
56 {
57 // Already an active participant
58 }
59 else if (inactiveParticipant != null)
60 {
61 inactiveParticipant.IsActive = true;
62 inactiveParticipant.LeftAt = null;
63 _uow.TripParticipants.Update(inactiveParticipant);
64 }
65 else
66 {
67 var participant = new TripParticipant
68 {
69 Id = Guid.NewGuid(),
70 TripId = invitation.TripId,
71 UserId = userId,
72 Role = EParticipantRole.Participant,
73 JoinedAt = DateTime.UtcNow,
74 IsActive = true
75 };
76 _uow.TripParticipants.Add(participant);
77 }
78
79 invitation.Status = EInvitationStatus.Accepted;
80 invitation.RespondedAt = DateTime.UtcNow;
81 _uow.TripInvitations.Update(invitation);
82
83 await _uow.SaveChangesAsync();
84
85 return true;
86 }
87
88 public async Task<(TripInvitationBllDto? invitation, string? errorCode)> CreateInvitationGuardedAsync(Guid tripId, Guid userId)
89 {
90 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId))
91 return (null, "forbidden");
92 var invitation = await CreateInvitationAsync(tripId, userId);
93 return (invitation, null);
94 }
95
96 public async Task<TripInvitationBllDto?> GetByIdAsync(Guid id)
97 {
98 var entity = await _uow.TripInvitations.GetByIdAsync(id);
99 return entity == null ? null : InvitationBllDtoFactory.Create(entity);
100 }
101
102 public async Task<TripInvitationBllDto?> GetByTokenAsync(string token)
103 {
104 var entity = await _uow.TripInvitations.GetByTokenAsync(token);
105 return entity == null ? null : InvitationBllDtoFactory.Create(entity);
106 }
107
108 public async Task<List<TripInvitationBllDto>> GetPendingByTripIdAsync(Guid tripId, Guid userId)
109 {
110 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId))
111 return new List<TripInvitationBllDto>();
112 var invitations = await _uow.TripInvitations.GetPendingByTripIdAsync(tripId);
113 return InvitationBllDtoFactory.CreateList(invitations);
114 }
115
116 public async Task<(bool success, string? errorCode)> AcceptInvitationGuardedAsync(string token, Guid userId)
117 {
118 var invitation = await _uow.TripInvitations.GetByTokenAsync(token);
119 if (invitation == null) return (false, "notfound");
120
121 if (invitation.Status != EInvitationStatus.Pending)
122 return (false, "not-pending");
123
124 if (invitation.ExpiresAt < DateTime.UtcNow)
125 {
126 invitation.Status = EInvitationStatus.Expired;
127 _uow.TripInvitations.Update(invitation);
128 await _uow.SaveChangesAsync();
129 return (false, "expired");
130 }
131
132 if (await _uow.TripParticipants.IsParticipantAsync(invitation.TripId, userId))
133 return (false, "already-participant");
134
135 var accepted = await AcceptInvitationAsync(token, userId);
136 if (!accepted) return (false, "failed");
137
138 return (true, null);
139 }
140
141 public async Task<(bool success, string? errorCode)> RevokeInvitationAsync(Guid invitationId, Guid tripId, Guid userId)
142 {
143 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId))
144 return (false, "forbidden");
145
146 var invitation = await _uow.TripInvitations.GetByIdAsync(invitationId);
147 if (invitation == null || invitation.TripId != tripId) return (false, "notfound");
148
149 if (invitation.Status == EInvitationStatus.Pending)
150 {
151 invitation.Status = EInvitationStatus.Revoked;
152 invitation.RespondedAt = DateTime.UtcNow;
153 _uow.TripInvitations.Update(invitation);
154 await _uow.SaveChangesAsync();
155 }
156
157 return (true, null);
158 }
159
160 public async Task<(bool success, string? errorCode)> RevokeInvitationByTokenAsync(string token, Guid userId)
161 {
162 var invitation = await _uow.TripInvitations.GetByTokenAsync(token);
163 if (invitation == null) return (false, "notfound");
164
165 if (!await _uow.TripParticipants.IsOrganizerAsync(invitation.TripId, userId))
166 return (false, "forbidden");
167
168 invitation.Status = EInvitationStatus.Revoked;
169 invitation.RespondedAt = DateTime.UtcNow;
170 _uow.TripInvitations.Update(invitation);
171 await _uow.SaveChangesAsync();
172 return (true, null);
173 }
174
175 public async Task<(bool success, string? errorCode)> DeclineInvitationAsync(string token)
176 {
177 var invitation = await _uow.TripInvitations.GetByTokenAsync(token);
178 if (invitation == null) return (false, "notfound");
179
180 if (invitation.Status != EInvitationStatus.Pending)
181 return (false, "not-pending");
182
183 invitation.Status = EInvitationStatus.Declined;
184 invitation.RespondedAt = DateTime.UtcNow;
185 _uow.TripInvitations.Update(invitation);
186 await _uow.SaveChangesAsync();
187 return (true, null);
188 }
189 }
190