profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

main default branch 501 files Expires Sep 13, 2026, 9:06 AM
AppUnitOfWork.cs 25,263 bytes
1 using Microsoft.EntityFrameworkCore;
2 using SplitApp.Modules.Expenses.Domain.Entities;
3 using SplitApp.Modules.Expenses.Infrastructure.Persistence;
4 using SplitApp.Modules.Trips.Domain.Entities;
5 using SplitApp.Modules.Trips.Infrastructure.Persistence;
6 using SplitApp.Shared.Contracts.Users;
7 using SplitApp.Shared.Kernel.Domain;
8 using SplitApp.Shared.Kernel.Persistence;
9 using SplitApp.Shared.Messaging.Integration.Users;
10 using SplitApp.WebApp.Application.Contracts;
11
12 namespace SplitApp.WebApp.Application.Persistence;
13
14 /// <summary>
15 /// Composition-root unit-of-work that aggregates the Trips + Expenses module DbContexts.
16 /// The Users module now lives in a separate process (SplitApp.UsersService) — user data
17 /// is fetched via <see cref="IUserLookup"/> (RabbitMQ RPC) and projected as UserDto into
18 /// the [NotMapped] cross-module nav properties on Trip/Expense entities.
19 /// </summary>
20 public class AppUnitOfWork : IAppUnitOfWork
21 {
22 private readonly TripsDbContext _tripsDb;
23 private readonly ExpensesDbContext _expensesDb;
24 private readonly IUserLookup _users;
25
26 public AppUnitOfWork(TripsDbContext tripsDb, ExpensesDbContext expensesDb, IUserLookup users)
27 {
28 _tripsDb = tripsDb;
29 _expensesDb = expensesDb;
30 _users = users;
31
32 Trips = new TripRepo(tripsDb, _users, expensesDb);
33 Expenses = new ExpenseRepo(expensesDb, _users, tripsDb);
34 TripParticipants = new TripParticipantRepo(tripsDb, _users);
35 TripInvitations = new TripInvitationRepo(tripsDb, _users);
36 SettlementPlans = new SettlementPlanRepo(expensesDb, _users, tripsDb);
37 SettlementPayments = new SettlementPaymentRepo(expensesDb, _users);
38 TripPolls = new TripPollRepo(tripsDb, _users);
39 TripWishlistItems = new TripWishlistItemRepo(tripsDb, _users);
40 SplitPresets = new SplitPresetRepo(expensesDb, _users, tripsDb);
41 BudgetCategories = new BudgetCategoryRepo(tripsDb);
42 }
43
44 public ITripRepository Trips { get; }
45 public IExpenseRepository Expenses { get; }
46 public ITripParticipantRepository TripParticipants { get; }
47 public ITripInvitationRepository TripInvitations { get; }
48 public ISettlementPlanRepository SettlementPlans { get; }
49 public ISettlementPaymentRepository SettlementPayments { get; }
50 public ITripPollRepository TripPolls { get; }
51 public ITripWishlistItemRepository TripWishlistItems { get; }
52 public ISplitPresetRepository SplitPresets { get; }
53 public IBudgetCategoryRepository BudgetCategories { get; }
54
55 public async Task<int> SaveChangesAsync()
56 {
57 var trips = await _tripsDb.SaveChangesAsync();
58 var expenses = await _expensesDb.SaveChangesAsync();
59 return trips + expenses;
60 }
61
62 public IBaseRepository<TEntity> GetRepository<TEntity>() where TEntity : class, IBaseEntity
63 {
64 var t = typeof(TEntity);
65 if (t == typeof(SettlementPayment)) return (IBaseRepository<TEntity>)SettlementPayments;
66 if (t == typeof(SettlementPlan)) return (IBaseRepository<TEntity>)SettlementPlans;
67 if (t == typeof(Trip)) return (IBaseRepository<TEntity>)Trips;
68 if (t == typeof(TripParticipant)) return (IBaseRepository<TEntity>)TripParticipants;
69 if (t == typeof(Expense)) return (IBaseRepository<TEntity>)Expenses;
70 if (t == typeof(TripInvitation)) return (IBaseRepository<TEntity>)TripInvitations;
71 if (t == typeof(BudgetCategory)) return (IBaseRepository<TEntity>)BudgetCategories;
72 if (t == typeof(TripPoll)) return (IBaseRepository<TEntity>)TripPolls;
73 if (t == typeof(TripWishlistItem)) return (IBaseRepository<TEntity>)TripWishlistItems;
74 if (t == typeof(SplitPreset)) return (IBaseRepository<TEntity>)SplitPresets;
75 if (t == typeof(Currency)) return new GenericExpensesRepo<Currency>(_expensesDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
76 if (t == typeof(ExpenseSplit)) return new GenericExpensesRepo<ExpenseSplit>(_expensesDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
77 if (t == typeof(SplitPresetMember)) return new GenericExpensesRepo<SplitPresetMember>(_expensesDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
78 if (t == typeof(TripPollOption)) return new GenericTripsRepo<TripPollOption>(_tripsDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
79 if (t == typeof(TripPollVote)) return new GenericTripsRepo<TripPollVote>(_tripsDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
80 if (t == typeof(TripWishlistVote)) return new GenericTripsRepo<TripWishlistVote>(_tripsDb) as IBaseRepository<TEntity> ?? throw new InvalidOperationException();
81 throw new InvalidOperationException($"No repository registered for {t.Name}");
82 }
83 }
84
85 internal class GenericTripsRepo<TEntity> : GenericRepo<TEntity, TripsDbContext>
86 where TEntity : class, IBaseEntity
87 {
88 public GenericTripsRepo(TripsDbContext db) : base(db) { }
89 }
90
91 internal class GenericExpensesRepo<TEntity> : GenericRepo<TEntity, ExpensesDbContext>
92 where TEntity : class, IBaseEntity
93 {
94 public GenericExpensesRepo(ExpensesDbContext db) : base(db) { }
95 }
96
97 internal class GenericRepo<TEntity, TDbContext> : IBaseRepository<TEntity>
98 where TEntity : class, IBaseEntity
99 where TDbContext : DbContext
100 {
101 protected readonly TDbContext Db;
102 protected readonly DbSet<TEntity> Set;
103
104 protected GenericRepo(TDbContext db)
105 {
106 Db = db;
107 Set = db.Set<TEntity>();
108 }
109
110 public virtual async Task<IEnumerable<TEntity>> GetAllAsync() => await Set.ToListAsync();
111 public virtual async Task<TEntity?> GetByIdAsync(Guid id) => await Set.FirstOrDefaultAsync(e => e.Id == id);
112 public virtual TEntity Add(TEntity entity) => Set.Add(entity).Entity;
113 public virtual TEntity Update(TEntity entity) => Set.Update(entity).Entity;
114 public virtual async Task<TEntity?> RemoveAsync(Guid id)
115 {
116 var e = await GetByIdAsync(id);
117 if (e == null) return null;
118 return Set.Remove(e).Entity;
119 }
120 public virtual async Task<bool> ExistsAsync(Guid id) => await Set.AnyAsync(e => e.Id == id);
121 }
122
123 internal static class CrossModuleHydration
124 {
125 /// <summary>Batches a single user-lookup over RabbitMQ, populates each item's User nav with UserDto.</summary>
126 public static async Task HydrateUsersAsync<T>(
127 IUserLookup userLookup,
128 IEnumerable<T> items,
129 Func<T, Guid> getUserId,
130 Action<T, UserDto?> setUser) where T : class
131 {
132 var list = items as IList<T> ?? items.ToList();
133 var ids = list.Select(getUserId).Where(id => id != Guid.Empty).Distinct().ToList();
134 if (ids.Count == 0) return;
135 var users = await userLookup.GetByIdsAsync(ids);
136 foreach (var item in list)
137 {
138 users.TryGetValue(getUserId(item), out var u);
139 setUser(item, u);
140 }
141 }
142
143 public static async Task HydrateUsersAsync<T>(
144 IUserLookup userLookup,
145 IEnumerable<T> items,
146 Func<T, Guid?> getUserId,
147 Action<T, UserDto?> setUser) where T : class
148 {
149 var list = items as IList<T> ?? items.ToList();
150 var ids = list.Select(getUserId).Where(id => id is { } x && x != Guid.Empty).Select(id => id!.Value).Distinct().ToList();
151 if (ids.Count == 0) return;
152 var users = await userLookup.GetByIdsAsync(ids);
153 foreach (var item in list)
154 {
155 var id = getUserId(item);
156 if (id.HasValue && users.TryGetValue(id.Value, out var u)) setUser(item, u);
157 else setUser(item, null);
158 }
159 }
160
161 public static async Task HydrateTripsAsync<T>(
162 TripsDbContext tripsDb,
163 IEnumerable<T> items,
164 Func<T, Guid> getTripId,
165 Action<T, Trip?> setTrip) where T : class
166 {
167 var list = items as IList<T> ?? items.ToList();
168 var ids = list.Select(getTripId).Where(id => id != Guid.Empty).Distinct().ToList();
169 if (ids.Count == 0) return;
170 var trips = await tripsDb.Trips
171 .Where(t => ids.Contains(t.Id))
172 .ToDictionaryAsync(t => t.Id);
173 foreach (var item in list)
174 {
175 trips.TryGetValue(getTripId(item), out var t);
176 setTrip(item, t);
177 }
178 }
179 }
180
181 internal class TripRepo : GenericRepo<Trip, TripsDbContext>, ITripRepository
182 {
183 private readonly IUserLookup _users;
184 private readonly ExpensesDbContext _expenses;
185
186 public TripRepo(TripsDbContext db, IUserLookup users, ExpensesDbContext expenses) : base(db)
187 {
188 _users = users;
189 _expenses = expenses;
190 }
191
192 private async Task HydrateTripAsync(Trip trip)
193 {
194 var creator = await _users.GetByIdAsync(trip.CreatedById);
195 trip.CreatedBy = creator;
196 trip.DefaultCurrency = await _expenses.Currencies.FirstOrDefaultAsync(c => c.Id == trip.DefaultCurrencyId);
197
198 if (trip.Participants is { Count: > 0 } parts)
199 await CrossModuleHydration.HydrateUsersAsync(_users, parts, p => p.UserId, (p, u) => p.User = u);
200 if (trip.Invitations is { Count: > 0 } invs)
201 await CrossModuleHydration.HydrateUsersAsync(_users, invs, i => i.InvitedByUserId, (i, u) => i.InvitedByUser = u);
202 if (trip.Polls is { Count: > 0 } polls)
203 await CrossModuleHydration.HydrateUsersAsync(_users, polls, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
204 if (trip.WishlistItems is { Count: > 0 } wls)
205 await CrossModuleHydration.HydrateUsersAsync(_users, wls, w => w.AddedByUserId, (w, u) => w.AddedByUser = u);
206 }
207
208 public override async Task<Trip?> GetByIdAsync(Guid id)
209 {
210 var trip = await base.GetByIdAsync(id);
211 if (trip != null) await HydrateTripAsync(trip);
212 return trip;
213 }
214
215 public override async Task<IEnumerable<Trip>> GetAllAsync()
216 {
217 var trips = (await Db.Trips.ToListAsync());
218 await CrossModuleHydration.HydrateUsersAsync(_users, trips, t => t.CreatedById, (t, u) => t.CreatedBy = u);
219 var currencyIds = trips.Select(t => t.DefaultCurrencyId).Where(id => id != Guid.Empty).Distinct().ToList();
220 if (currencyIds.Count > 0)
221 {
222 var currencies = await _expenses.Currencies
223 .Where(c => currencyIds.Contains(c.Id))
224 .ToDictionaryAsync(c => c.Id);
225 foreach (var t in trips)
226 {
227 if (currencies.TryGetValue(t.DefaultCurrencyId, out var c)) t.DefaultCurrency = c;
228 }
229 }
230 return trips;
231 }
232
233 public async Task<IEnumerable<Trip>> GetUserTripsAsync(Guid userId)
234 {
235 var trips = await Db.Trips
236 .Include(t => t.Participants)
237 .Where(t => t.CreatedById == userId
238 || (t.Participants != null
239 && t.Participants.Any(p => p.UserId == userId && p.IsActive)))
240 .ToListAsync();
241 foreach (var t in trips) await HydrateTripAsync(t);
242 return trips;
243 }
244
245 public async Task<Trip?> GetByIdWithDetailsAsync(Guid id)
246 {
247 var trip = await Db.Trips
248 .Include(t => t.Participants)
249 .Include(t => t.BudgetCategories)
250 .Include(t => t.WishlistItems)
251 .Include(t => t.Polls)!.ThenInclude(p => p.Options)
252 .Include(t => t.Invitations)
253 .FirstOrDefaultAsync(t => t.Id == id);
254 if (trip != null) await HydrateTripAsync(trip);
255 return trip;
256 }
257 }
258
259 internal class ExpenseRepo : GenericRepo<Expense, ExpensesDbContext>, IExpenseRepository
260 {
261 private readonly IUserLookup _users;
262 private readonly TripsDbContext _trips;
263 public ExpenseRepo(ExpensesDbContext db, IUserLookup users, TripsDbContext trips) : base(db)
264 {
265 _users = users;
266 _trips = trips;
267 }
268
269 public override async Task<IEnumerable<Expense>> GetAllAsync()
270 {
271 var expenses = await Db.Expenses.Include(e => e.Currency).ToListAsync();
272 await CrossModuleHydration.HydrateUsersAsync(_users, expenses, e => e.PaidByUserId, (e, u) => e.PaidByUser = u);
273 await CrossModuleHydration.HydrateTripsAsync(_trips, expenses, e => e.TripId, (e, t) => e.Trip = t);
274 return expenses;
275 }
276
277 public override async Task<Expense?> GetByIdAsync(Guid id)
278 {
279 var e = await Db.Expenses.Include(x => x.Currency).FirstOrDefaultAsync(x => x.Id == id);
280 if (e != null)
281 {
282 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { e }, x => x.PaidByUserId, (x, u) => x.PaidByUser = u);
283 await CrossModuleHydration.HydrateTripsAsync(_trips, new[] { e }, x => x.TripId, (x, t) => x.Trip = t);
284 }
285 return e;
286 }
287
288 public async Task<IEnumerable<Expense>> GetByTripIdAsync(Guid tripId)
289 {
290 var expenses = await Db.Expenses
291 .Include(e => e.Currency)
292 .Where(e => e.TripId == tripId)
293 .ToListAsync();
294 await CrossModuleHydration.HydrateUsersAsync(_users, expenses, e => e.PaidByUserId, (e, u) => e.PaidByUser = u);
295 return expenses;
296 }
297
298 public async Task<Expense?> GetByIdWithDetailsAsync(Guid id)
299 {
300 var expense = await Db.Expenses
301 .Include(e => e.Currency)
302 .Include(e => e.Splits)
303 .FirstOrDefaultAsync(e => e.Id == id);
304 if (expense == null) return null;
305
306 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { expense }, e => e.PaidByUserId, (e, u) => e.PaidByUser = u);
307 if (expense.Splits is { Count: > 0 } splits)
308 await CrossModuleHydration.HydrateUsersAsync(_users, splits, s => s.UserId, (s, u) => s.User = u);
309 return expense;
310 }
311 }
312
313 internal class TripParticipantRepo : GenericRepo<TripParticipant, TripsDbContext>, ITripParticipantRepository
314 {
315 private readonly IUserLookup _users;
316 public TripParticipantRepo(TripsDbContext db, IUserLookup users) : base(db) { _users = users; }
317
318 public async Task<bool> IsParticipantAsync(Guid tripId, Guid userId)
319 => await Db.TripParticipants.AnyAsync(p => p.TripId == tripId && p.UserId == userId && p.IsActive);
320
321 public async Task<bool> IsOrganizerAsync(Guid tripId, Guid userId)
322 {
323 var trip = await Db.Trips.FirstOrDefaultAsync(t => t.Id == tripId);
324 if (trip == null) return false;
325 if (trip.CreatedById == userId) return true;
326 return await Db.TripParticipants.AnyAsync(p =>
327 p.TripId == tripId && p.UserId == userId && p.IsActive
328 && p.Role == SplitApp.Modules.Trips.Domain.Enums.EParticipantRole.Organizer);
329 }
330
331 public async Task<IEnumerable<TripParticipant>> GetByTripIdAsync(Guid tripId)
332 {
333 var parts = await Db.TripParticipants.Where(p => p.TripId == tripId).ToListAsync();
334 await CrossModuleHydration.HydrateUsersAsync(_users, parts, p => p.UserId, (p, u) => p.User = u);
335 return parts;
336 }
337
338 public override async Task<TripParticipant?> GetByIdAsync(Guid id)
339 {
340 var p = await Db.TripParticipants.Include(x => x.Trip).FirstOrDefaultAsync(x => x.Id == id);
341 if (p != null)
342 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { p }, x => x.UserId, (x, u) => x.User = u);
343 return p;
344 }
345
346 public override async Task<IEnumerable<TripParticipant>> GetAllAsync()
347 {
348 var parts = await Db.TripParticipants.Include(p => p.Trip).ToListAsync();
349 await CrossModuleHydration.HydrateUsersAsync(_users, parts, p => p.UserId, (p, u) => p.User = u);
350 return parts;
351 }
352 }
353
354 internal class TripInvitationRepo : GenericRepo<TripInvitation, TripsDbContext>, ITripInvitationRepository
355 {
356 private readonly IUserLookup _users;
357 public TripInvitationRepo(TripsDbContext db, IUserLookup users) : base(db) { _users = users; }
358
359 public override async Task<IEnumerable<TripInvitation>> GetAllAsync()
360 {
361 var invs = await Db.TripInvitations.Include(i => i.Trip).ToListAsync();
362 await CrossModuleHydration.HydrateUsersAsync(_users, invs, i => i.InvitedByUserId, (i, u) => i.InvitedByUser = u);
363 return invs;
364 }
365
366 public async Task<TripInvitation?> GetByTokenAsync(string token)
367 {
368 var inv = await Db.TripInvitations.Include(i => i.Trip).FirstOrDefaultAsync(i => i.Token == token);
369 if (inv != null)
370 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { inv }, i => i.InvitedByUserId, (i, u) => i.InvitedByUser = u);
371 return inv;
372 }
373
374 public async Task<IEnumerable<TripInvitation>> GetPendingByTripIdAsync(Guid tripId)
375 {
376 var invs = await Db.TripInvitations
377 .Where(i => i.TripId == tripId
378 && i.Status == SplitApp.Modules.Trips.Domain.Enums.EInvitationStatus.Pending)
379 .ToListAsync();
380 await CrossModuleHydration.HydrateUsersAsync(_users, invs, i => i.InvitedByUserId, (i, u) => i.InvitedByUser = u);
381 return invs;
382 }
383 }
384
385 internal class SettlementPlanRepo : GenericRepo<SettlementPlan, ExpensesDbContext>, ISettlementPlanRepository
386 {
387 private readonly IUserLookup _users;
388 private readonly TripsDbContext _trips;
389 public SettlementPlanRepo(ExpensesDbContext db, IUserLookup users, TripsDbContext trips) : base(db)
390 {
391 _users = users;
392 _trips = trips;
393 }
394
395 public override async Task<IEnumerable<SettlementPlan>> GetAllAsync()
396 {
397 var plans = await Db.SettlementPlans.ToListAsync();
398 await CrossModuleHydration.HydrateUsersAsync(_users, plans, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
399 await CrossModuleHydration.HydrateTripsAsync(_trips, plans, p => p.TripId, (p, t) => p.Trip = t);
400 return plans;
401 }
402
403 private async Task HydratePlanAsync(SettlementPlan plan)
404 {
405 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { plan }, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
406 await CrossModuleHydration.HydrateTripsAsync(_trips, new[] { plan }, p => p.TripId, (p, t) => p.Trip = t);
407 if (plan.Payments is { Count: > 0 } payments)
408 {
409 var ids = payments.SelectMany(p => new[] { p.FromUserId, p.ToUserId })
410 .Where(id => id != Guid.Empty).Distinct().ToList();
411 if (ids.Count > 0)
412 {
413 var users = await _users.GetByIdsAsync(ids);
414 foreach (var p in payments)
415 {
416 if (users.TryGetValue(p.FromUserId, out var fu)) p.FromUser = fu;
417 if (users.TryGetValue(p.ToUserId, out var tu)) p.ToUser = tu;
418 }
419 }
420 }
421 }
422
423 public override async Task<SettlementPlan?> GetByIdAsync(Guid id)
424 {
425 var plan = await Db.SettlementPlans.Include(p => p.Payments).FirstOrDefaultAsync(p => p.Id == id);
426 if (plan != null) await HydratePlanAsync(plan);
427 return plan;
428 }
429
430 public async Task<SettlementPlan?> GetLatestByTripIdAsync(Guid tripId)
431 {
432 var plan = await Db.SettlementPlans
433 .Include(p => p.Payments)
434 .Where(p => p.TripId == tripId)
435 .OrderByDescending(p => p.CreatedAt)
436 .FirstOrDefaultAsync();
437 if (plan != null) await HydratePlanAsync(plan);
438 return plan;
439 }
440
441 public async Task DeletePlanWithPaymentsAsync(Guid planId)
442 {
443 var payments = await Db.SettlementPayments.Where(p => p.SettlementPlanId == planId).ToListAsync();
444 Db.SettlementPayments.RemoveRange(payments);
445 var plan = await Db.SettlementPlans.FirstOrDefaultAsync(p => p.Id == planId);
446 if (plan != null) Db.SettlementPlans.Remove(plan);
447 await Db.SaveChangesAsync();
448 }
449 }
450
451 internal class SettlementPaymentRepo : GenericRepo<SettlementPayment, ExpensesDbContext>, ISettlementPaymentRepository
452 {
453 private readonly IUserLookup _users;
454 public SettlementPaymentRepo(ExpensesDbContext db, IUserLookup users) : base(db) { _users = users; }
455
456 public override async Task<IEnumerable<SettlementPayment>> GetAllAsync()
457 {
458 var payments = await Db.SettlementPayments.ToListAsync();
459 var ids = payments.SelectMany(p => new[] { p.FromUserId, p.ToUserId })
460 .Where(id => id != Guid.Empty).Distinct().ToList();
461 if (ids.Count > 0)
462 {
463 var users = await _users.GetByIdsAsync(ids);
464 foreach (var p in payments)
465 {
466 if (users.TryGetValue(p.FromUserId, out var fu)) p.FromUser = fu;
467 if (users.TryGetValue(p.ToUserId, out var tu)) p.ToUser = tu;
468 }
469 }
470 return payments;
471 }
472
473 public override async Task<SettlementPayment?> GetByIdAsync(Guid id)
474 {
475 var p = await base.GetByIdAsync(id);
476 if (p != null)
477 {
478 var ids = new[] { p.FromUserId, p.ToUserId }.Where(x => x != Guid.Empty).Distinct().ToList();
479 var users = await _users.GetByIdsAsync(ids);
480 if (users.TryGetValue(p.FromUserId, out var fu)) p.FromUser = fu;
481 if (users.TryGetValue(p.ToUserId, out var tu)) p.ToUser = tu;
482 }
483 return p;
484 }
485 }
486
487 internal class TripPollRepo : GenericRepo<TripPoll, TripsDbContext>, ITripPollRepository
488 {
489 private readonly IUserLookup _users;
490 public TripPollRepo(TripsDbContext db, IUserLookup users) : base(db) { _users = users; }
491
492 public override async Task<IEnumerable<TripPoll>> GetAllAsync()
493 {
494 var polls = await Db.TripPolls.Include(p => p.Trip).ToListAsync();
495 await CrossModuleHydration.HydrateUsersAsync(_users, polls, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
496 return polls;
497 }
498
499 public async Task<IEnumerable<TripPoll>> GetByTripIdAsync(Guid tripId)
500 {
501 var polls = await Db.TripPolls
502 .Include(p => p.Options)!
503 .ThenInclude(o => o.Votes)
504 .Where(p => p.TripId == tripId)
505 .ToListAsync();
506 await CrossModuleHydration.HydrateUsersAsync(_users, polls, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
507 return polls;
508 }
509
510 public async Task<TripPoll?> GetByIdWithDetailsAsync(Guid id)
511 {
512 var poll = await Db.TripPolls
513 .Include(p => p.Options)!
514 .ThenInclude(o => o.Votes)
515 .FirstOrDefaultAsync(p => p.Id == id);
516 if (poll != null)
517 await CrossModuleHydration.HydrateUsersAsync(_users, new[] { poll }, p => p.CreatedByUserId, (p, u) => p.CreatedByUser = u);
518 return poll;
519 }
520 }
521
522 internal class TripWishlistItemRepo : GenericRepo<TripWishlistItem, TripsDbContext>, ITripWishlistItemRepository
523 {
524 private readonly IUserLookup _users;
525 public TripWishlistItemRepo(TripsDbContext db, IUserLookup users) : base(db) { _users = users; }
526
527 public override async Task<IEnumerable<TripWishlistItem>> GetAllAsync()
528 {
529 var items = await Db.TripWishlistItems.Include(i => i.Trip).ToListAsync();
530 await CrossModuleHydration.HydrateUsersAsync(_users, items, i => i.AddedByUserId, (i, u) => i.AddedByUser = u);
531 return items;
532 }
533
534 public async Task<IEnumerable<TripWishlistItem>> GetByTripIdAsync(Guid tripId)
535 {
536 var items = await Db.TripWishlistItems
537 .Include(i => i.Votes)
538 .Where(i => i.TripId == tripId)
539 .ToListAsync();
540 await CrossModuleHydration.HydrateUsersAsync(_users, items, i => i.AddedByUserId, (i, u) => i.AddedByUser = u);
541 return items;
542 }
543 }
544
545 internal class SplitPresetRepo : GenericRepo<SplitPreset, ExpensesDbContext>, ISplitPresetRepository
546 {
547 private readonly IUserLookup _users;
548 private readonly TripsDbContext _trips;
549 public SplitPresetRepo(ExpensesDbContext db, IUserLookup users, TripsDbContext trips) : base(db)
550 {
551 _users = users;
552 _trips = trips;
553 }
554
555 public override async Task<IEnumerable<SplitPreset>> GetAllAsync()
556 {
557 var presets = await Db.SplitPresets.ToListAsync();
558 await CrossModuleHydration.HydrateUsersAsync(_users, presets, p => p.CreatedById, (p, u) => p.CreatedBy = u);
559 await CrossModuleHydration.HydrateTripsAsync(_trips, presets, p => p.TripId, (p, t) => p.Trip = t);
560 return presets;
561 }
562
563 public async Task<IEnumerable<SplitPreset>> GetByTripIdAsync(Guid tripId)
564 {
565 var presets = await Db.SplitPresets
566 .Include(p => p.Members)
567 .Where(p => p.TripId == tripId)
568 .ToListAsync();
569 await CrossModuleHydration.HydrateUsersAsync(_users, presets, p => p.CreatedById, (p, u) => p.CreatedBy = u);
570 var members = presets.SelectMany(p => p.Members ?? Enumerable.Empty<SplitPresetMember>()).ToList();
571 if (members.Count > 0)
572 await CrossModuleHydration.HydrateUsersAsync(_users, members, m => m.UserId, (m, u) => m.User = u);
573 return presets;
574 }
575 }
576
577 internal class BudgetCategoryRepo : GenericRepo<BudgetCategory, TripsDbContext>, IBudgetCategoryRepository
578 {
579 public BudgetCategoryRepo(TripsDbContext db) : base(db) { }
580
581 public async Task<IEnumerable<BudgetCategory>> GetByTripIdAsync(Guid tripId)
582 => await Db.BudgetCategories.Where(c => c.TripId == tripId).ToListAsync();
583 }
584