profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

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