profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
TripService.cs 8,674 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 TripService : ITripService
9 {
10 private readonly IAppUnitOfWork _uow;
11 private readonly ISettlementService _settlementService;
12
13 public TripService(IAppUnitOfWork uow, ISettlementService settlementService)
14 {
15 _uow = uow;
16 _settlementService = settlementService;
17 }
18
19 public async Task<TripBllDto> CreateTripAsync(TripBllDto trip, Guid userId)
20 {
21 var entity = TripBllDtoFactory.ToEntity(trip);
22 entity.Id = Guid.NewGuid();
23 entity.CreatedById = userId;
24 entity.Status = ETripStatus.Active;
25
26 _uow.Trips.Add(entity);
27
28 var participant = new TripParticipant
29 {
30 Id = Guid.NewGuid(),
31 TripId = entity.Id,
32 UserId = userId,
33 Role = EParticipantRole.Organizer,
34 JoinedAt = DateTime.UtcNow,
35 IsActive = true
36 };
37
38 _uow.TripParticipants.Add(participant);
39
40 await _uow.SaveChangesAsync();
41
42 return TripBllDtoFactory.Create(entity);
43 }
44
45 public async Task<List<TripBllDto>> GetUserTripsAsync(Guid userId)
46 {
47 var trips = await _uow.Trips.GetUserTripsAsync(userId);
48 return TripBllDtoFactory.CreateList(trips, includeParticipants: true);
49 }
50
51 public async Task<TripBllDto?> GetByIdAsync(Guid tripId, Guid userId)
52 {
53 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId)) return null;
54 var trip = await _uow.Trips.GetByIdAsync(tripId);
55 return trip == null ? null : TripBllDtoFactory.Create(trip);
56 }
57
58 public async Task<TripBllDto?> GetByIdWithDetailsAsync(Guid tripId, Guid userId)
59 {
60 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId)) return null;
61 var trip = await _uow.Trips.GetByIdWithDetailsAsync(tripId);
62 return trip == null ? null : TripBllDtoFactory.Create(trip, includeParticipants: true, includeExpenses: true);
63 }
64
65 public async Task<TripBllDto?> GetByIdForOrganizerAsync(Guid tripId, Guid userId)
66 {
67 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId)) return null;
68 var trip = await _uow.Trips.GetByIdAsync(tripId);
69 return trip == null ? null : TripBllDtoFactory.Create(trip);
70 }
71
72 public async Task<TripBllDto?> GetRawByIdAsync(Guid tripId)
73 {
74 var trip = await _uow.Trips.GetByIdAsync(tripId);
75 return trip == null ? null : TripBllDtoFactory.Create(trip);
76 }
77
78 public Task<bool> IsParticipantAsync(Guid tripId, Guid userId)
79 => _uow.TripParticipants.IsParticipantAsync(tripId, userId);
80
81 public Task<bool> IsOrganizerAsync(Guid tripId, Guid userId)
82 => _uow.TripParticipants.IsOrganizerAsync(tripId, userId);
83
84 public async Task<TripBllDto?> UpdateAsync(TripBllDto trip, Guid userId)
85 {
86 if (!await _uow.TripParticipants.IsOrganizerAsync(trip.Id, userId)) return null;
87
88 var existing = await _uow.Trips.GetByIdAsync(trip.Id);
89 if (existing == null) return null;
90
91 existing.Name = trip.Name;
92 existing.Description = trip.Description;
93 existing.Destination = trip.Destination;
94 existing.StartDate = trip.StartDate;
95 existing.EndDate = trip.EndDate;
96 existing.DefaultCurrencyId = trip.DefaultCurrencyId;
97 existing.Status = trip.Status;
98
99 _uow.Trips.Update(existing);
100 await _uow.SaveChangesAsync();
101 return TripBllDtoFactory.Create(existing);
102 }
103
104 public async Task<bool> DeleteAsync(Guid tripId, Guid userId)
105 {
106 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId)) return false;
107
108 var trip = await _uow.Trips.GetByIdAsync(tripId);
109 if (trip == null) return false;
110
111 await _uow.Trips.RemoveAsync(tripId);
112 await _uow.SaveChangesAsync();
113 return true;
114 }
115
116 public async Task<List<TripParticipantBllDto>> GetParticipantsAsync(Guid tripId, Guid userId)
117 {
118 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId))
119 return new List<TripParticipantBllDto>();
120 var participants = await _uow.TripParticipants.GetByTripIdAsync(tripId);
121 return TripParticipantBllDtoFactory.CreateList(participants);
122 }
123
124 public Task<List<TripParticipantBllDto>> GetParticipantsForIndexAsync(Guid tripId, Guid userId)
125 => GetParticipantsAsync(tripId, userId);
126
127 public async Task<TripParticipantBllDto?> GetParticipantByIdAsync(Guid participantId)
128 {
129 var participant = await _uow.TripParticipants.GetByIdAsync(participantId);
130 return participant == null ? null : TripParticipantBllDtoFactory.Create(participant);
131 }
132
133 public async Task<(bool success, string? errorCode)> RemoveParticipantAsync(Guid tripId, Guid participantUserId, Guid currentUserId)
134 {
135 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, currentUserId))
136 return (false, "forbidden");
137
138 var participants = (await _uow.TripParticipants.GetByTripIdAsync(tripId)).ToList();
139 var participant = participants.FirstOrDefault(tp => tp.UserId == participantUserId);
140 if (participant == null) return (false, "notfound");
141
142 if (participant.Role == EParticipantRole.Organizer)
143 return (false, "organizer");
144 if (participant.UserId == currentUserId)
145 return (false, "self");
146
147 participant.IsActive = false;
148 participant.LeftAt = DateTime.UtcNow;
149 _uow.TripParticipants.Update(participant);
150 await _uow.SaveChangesAsync();
151
152 return (true, null);
153 }
154
155 public async Task<bool> RemoveParticipantByIdAsync(Guid tripId, Guid participantId, Guid currentUserId)
156 {
157 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, currentUserId))
158 return false;
159
160 var participant = await _uow.TripParticipants.GetByIdAsync(participantId);
161 if (participant == null || participant.TripId != tripId) return false;
162
163 // Cannot remove yourself
164 if (participant.UserId == currentUserId) return false;
165
166 participant.IsActive = false;
167 participant.LeftAt = DateTime.UtcNow;
168 _uow.TripParticipants.Update(participant);
169 await _uow.SaveChangesAsync();
170
171 return true;
172 }
173
174 public async Task<(bool success, string? errorCode)> FinalizeTripAsync(Guid tripId, Guid userId)
175 {
176 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId))
177 return (false, "forbidden");
178
179 var trip = await _uow.Trips.GetByIdAsync(tripId);
180 if (trip == null) return (false, "notfound");
181 if (trip.Status != ETripStatus.Active) return (false, "badstatus");
182
183 trip.Status = ETripStatus.Finalizing;
184 _uow.Trips.Update(trip);
185 await _uow.SaveChangesAsync();
186
187 // Auto-create settlement plan
188 var createdPlan = await _settlementService.CalculateSettlementAsync(tripId, userId);
189
190 // If nobody owes anyone, skip straight to Settled.
191 if (createdPlan == null)
192 {
193 trip.Status = ETripStatus.Settled;
194 await _uow.SaveChangesAsync();
195 }
196
197 return (true, null);
198 }
199
200 public async Task<(bool success, string? errorCode)> ReopenTripAsync(Guid tripId, Guid userId)
201 {
202 if (!await _uow.TripParticipants.IsOrganizerAsync(tripId, userId))
203 return (false, "forbidden");
204
205 var trip = await _uow.Trips.GetByIdAsync(tripId);
206 if (trip == null) return (false, "notfound");
207 if (trip.Status != ETripStatus.Finalizing && trip.Status != ETripStatus.Settled)
208 return (false, "badstatus");
209
210 var plan = await _uow.SettlementPlans.GetLatestByTripIdAsync(tripId);
211 if (plan?.Payments != null && plan.Payments.Any(p => p.Status == EPaymentStatus.Confirmed))
212 return (false, "payments-confirmed");
213
214 if (plan != null)
215 {
216 await _uow.SettlementPlans.DeletePlanWithPaymentsAsync(plan.Id);
217 }
218
219 var tripToUpdate = await _uow.Trips.GetByIdAsync(tripId);
220 if (tripToUpdate == null) return (false, "notfound");
221 tripToUpdate.Status = ETripStatus.Active;
222 _uow.Trips.Update(tripToUpdate);
223 await _uow.SaveChangesAsync();
224
225 return (true, null);
226 }
227
228 public async Task<List<CurrencyBllDto>> GetAllCurrenciesAsync()
229 {
230 var currencies = await _uow.GetRepository<Currency>().GetAllAsync();
231 return currencies
232 .OrderBy(c => c.Code)
233 .Select(CurrencyBllDtoFactory.Create)
234 .ToList();
235 }
236 }
237