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 | |