profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
AppDataInit.cs 29,827 bytes
1 using App.Domain;
2 using App.Domain.Identity;
3 using Base.Domain;
4 using Microsoft.AspNetCore.Identity;
5 using Microsoft.EntityFrameworkCore;
6
7 namespace App.DAL.EF.Seeding;
8
9 public static class AppDataInit
10 {
11 public static void DeleteDatabase(AppDbContext context)
12 {
13 context.Database.EnsureDeleted();
14 }
15
16 public static void MigrateDatabase(AppDbContext context)
17 {
18 context.Database.Migrate();
19 }
20
21 public static void SeedIdentity(UserManager<AppUser> userManager, RoleManager<AppRole> roleManager)
22 {
23 foreach (var roleName in InitialData.Roles)
24 {
25 var role = roleManager.FindByNameAsync(roleName).Result;
26 if (role != null) continue;
27
28 role = new AppRole { Name = roleName };
29 var result = roleManager.CreateAsync(role).Result;
30 if (!result.Succeeded)
31 {
32 throw new ApplicationException($"Role creation failed: {roleName}");
33 }
34 }
35
36 foreach (var userData in InitialData.SeedUsers())
37 {
38 var user = userManager.FindByEmailAsync(userData.email).Result;
39 if (user != null) continue;
40
41 user = new AppUser
42 {
43 Email = userData.email,
44 UserName = userData.email,
45 FirstName = userData.firstName,
46 LastName = userData.lastName,
47 EmailConfirmed = true,
48 };
49
50 // Allow overriding the seeded admin password via env (e.g. in production).
51 // Falls back to the built-in value so local development is unaffected.
52 var password = userData.password;
53 if (userData.roles.Contains("admin"))
54 {
55 var envPassword = Environment.GetEnvironmentVariable("SEED_ADMIN_PASSWORD");
56 if (!string.IsNullOrWhiteSpace(envPassword))
57 {
58 password = envPassword;
59 }
60 }
61
62 var result = userManager.CreateAsync(user, password).Result;
63 if (!result.Succeeded)
64 {
65 throw new ApplicationException($"User creation failed: {userData.email}");
66 }
67
68 foreach (var roleName in userData.roles)
69 {
70 var roleResult = userManager.AddToRoleAsync(user, roleName).Result;
71 if (!roleResult.Succeeded)
72 {
73 throw new ApplicationException($"Role assignment failed: {userData.email} -> {roleName}");
74 }
75 }
76 }
77 }
78
79 public static void SeedAppData(AppDbContext context)
80 {
81 // Seed currencies
82 if (!context.Currencies.Any())
83 {
84 foreach (var currencyData in InitialData.Currencies)
85 {
86 var name = new LangStr(currencyData.NameEn, "en");
87 name.SetTranslation(currencyData.NameEt, "et");
88
89 context.Currencies.Add(new Currency
90 {
91 Code = currencyData.Code,
92 Name = name,
93 Symbol = currencyData.Symbol,
94 });
95 }
96
97 context.SaveChanges();
98 }
99
100 // Seed example trips, participants, expenses, polls, wishlist items
101 if (!context.Trips.Any())
102 {
103 SeedExampleData(context);
104 }
105 }
106
107 private static void SeedExampleData(AppDbContext context)
108 {
109 // Get users
110 var admin = context.Users.First(u => u.Email == "admin@taltech.ee");
111 var testUser = context.Users.First(u => u.Email == "user@taltech.ee");
112 var alice = context.Users.First(u => u.Email == "alice@taltech.ee");
113 var bob = context.Users.First(u => u.Email == "bob@taltech.ee");
114 var charlie = context.Users.First(u => u.Email == "charlie@taltech.ee");
115 var diana = context.Users.First(u => u.Email == "diana@taltech.ee");
116
117 var eur = context.Currencies.First(c => c.Code == "EUR");
118 var usd = context.Currencies.First(c => c.Code == "USD");
119 var gbp = context.Currencies.First(c => c.Code == "GBP");
120
121 // ============================================================
122 // TRIP 1: Barcelona Weekend (Active, 4 participants, lots of expenses)
123 // ============================================================
124 var trip1 = new Trip
125 {
126 Name = "Barcelona Weekend",
127 Description = "A long weekend exploring Barcelona with friends",
128 Destination = "Barcelona, Spain",
129 StartDate = new DateTime(2026, 4, 10, 0, 0, 0, DateTimeKind.Utc),
130 EndDate = new DateTime(2026, 4, 13, 0, 0, 0, DateTimeKind.Utc),
131 Status = ETripStatus.Active,
132 DefaultCurrencyId = eur.Id,
133 CreatedById = admin.Id,
134 };
135 context.Trips.Add(trip1);
136
137 var tp1Admin = new TripParticipant
138 {
139 TripId = trip1.Id, UserId = admin.Id,
140 Role = EParticipantRole.Organizer, JoinedAt = DateTime.UtcNow.AddDays(-10), IsActive = true
141 };
142 var tp1Alice = new TripParticipant
143 {
144 TripId = trip1.Id, UserId = alice.Id,
145 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-9), IsActive = true
146 };
147 var tp1Bob = new TripParticipant
148 {
149 TripId = trip1.Id, UserId = bob.Id,
150 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-8), IsActive = true
151 };
152 var tp1Charlie = new TripParticipant
153 {
154 TripId = trip1.Id, UserId = charlie.Id,
155 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-7), IsActive = true
156 };
157 context.TripParticipants.AddRange(tp1Admin, tp1Alice, tp1Bob, tp1Charlie);
158
159 // Budget categories for trip 1 (with planned spending limits per proposal)
160 var cat1Food = new BudgetCategory { TripId = trip1.Id, Name = new LangStr("Food & Drinks", "en") { ["et"] = "Toit ja joogid" }, IconName = "cup-hot", PlannedAmount = 400m, DisplayOrder = 0 };
161 var cat1Transport = new BudgetCategory { TripId = trip1.Id, Name = new LangStr("Transport", "en") { ["et"] = "Transport" }, IconName = "bus-front", PlannedAmount = 150m, DisplayOrder = 1 };
162 var cat1Activities = new BudgetCategory { TripId = trip1.Id, Name = new LangStr("Activities", "en") { ["et"] = "Tegevused" }, IconName = "binoculars", PlannedAmount = 200m, DisplayOrder = 2 };
163 var cat1Accommodation = new BudgetCategory { TripId = trip1.Id, Name = new LangStr("Accommodation", "en") { ["et"] = "Majutus" }, IconName = "house", PlannedAmount = 500m, DisplayOrder = 3 };
164 context.BudgetCategories.AddRange(cat1Food, cat1Transport, cat1Activities, cat1Accommodation);
165
166 // Split presets for trip 1
167 var preset1All = new SplitPreset
168 {
169 TripId = trip1.Id, Name = "Everyone equal", SplitMethod = ESplitMethod.EqualAll,
170 CreatedById = admin.Id
171 };
172 var preset1Hotel = new SplitPreset
173 {
174 TripId = trip1.Id, Name = "Hotel group", SplitMethod = ESplitMethod.EqualSubset,
175 CreatedById = admin.Id
176 };
177 context.SplitPresets.AddRange(preset1All, preset1Hotel);
178
179 // Members for "Hotel group" preset (3 of 4 participants)
180 context.SplitPresetMembers.AddRange(
181 new SplitPresetMember { SplitPresetId = preset1Hotel.Id, UserId = admin.Id },
182 new SplitPresetMember { SplitPresetId = preset1Hotel.Id, UserId = alice.Id },
183 new SplitPresetMember { SplitPresetId = preset1Hotel.Id, UserId = bob.Id }
184 );
185
186 // Expenses for trip 1
187 var expenses1 = new List<(string desc, decimal amount, Guid paidBy, Guid? catId, DateTime date, ESplitMethod split)>
188 {
189 ("Airbnb apartment (3 nights)", 480.00m, admin.Id, cat1Accommodation.Id, DateTime.UtcNow.AddDays(-5), ESplitMethod.EqualAll),
190 ("Airport taxi", 35.00m, alice.Id, cat1Transport.Id, DateTime.UtcNow.AddDays(-5), ESplitMethod.EqualAll),
191 ("Grocery shopping", 62.50m, bob.Id, cat1Food.Id, DateTime.UtcNow.AddDays(-4), ESplitMethod.EqualAll),
192 ("Dinner at La Boqueria", 128.00m, admin.Id, cat1Food.Id, DateTime.UtcNow.AddDays(-4), ESplitMethod.EqualAll),
193 ("Sagrada Familia tickets", 104.00m, charlie.Id, cat1Activities.Id, DateTime.UtcNow.AddDays(-3), ESplitMethod.EqualAll),
194 ("Metro passes (4x)", 44.00m, alice.Id, cat1Transport.Id, DateTime.UtcNow.AddDays(-3), ESplitMethod.EqualAll),
195 ("Tapas bar lunch", 76.00m, bob.Id, cat1Food.Id, DateTime.UtcNow.AddDays(-3), ESplitMethod.EqualAll),
196 ("Park Guell entry", 40.00m, admin.Id, cat1Activities.Id, DateTime.UtcNow.AddDays(-2), ESplitMethod.EqualAll),
197 ("Sangria and snacks", 48.50m, charlie.Id, cat1Food.Id, DateTime.UtcNow.AddDays(-2), ESplitMethod.EqualAll),
198 ("Souvenir shopping", 55.00m, alice.Id, cat1Food.Id, DateTime.UtcNow.AddDays(-1), ESplitMethod.EqualSubset),
199 ("Return taxi to airport", 38.00m, bob.Id, cat1Transport.Id, DateTime.UtcNow.AddDays(-1), ESplitMethod.EqualAll),
200 };
201
202 var trip1Participants = new[] { admin.Id, alice.Id, bob.Id, charlie.Id };
203
204 foreach (var (desc, amount, paidBy, catId, date, split) in expenses1)
205 {
206 var expense = new Expense
207 {
208 TripId = trip1.Id,
209 PaidByUserId = paidBy,
210 Amount = amount,
211 Description = desc,
212 ExpenseDate = date,
213 BudgetCategoryId = catId,
214 CurrencyId = eur.Id,
215 SplitMethod = split,
216 };
217 context.Expenses.Add(expense);
218
219 // Create equal splits among all 4 participants
220 var splitParticipants = split == ESplitMethod.EqualSubset
221 ? new[] { alice.Id, bob.Id, charlie.Id } // souvenir shopping - only 3
222 : trip1Participants;
223
224 var count = splitParticipants.Length;
225 var baseAmt = Math.Floor(amount / count * 100) / 100;
226 var remainder = amount - baseAmt * count;
227
228 for (var i = 0; i < count; i++)
229 {
230 var splitAmt = baseAmt + (i < (int)(remainder * 100) ? 0.01m : 0);
231 context.ExpenseSplits.Add(new ExpenseSplit
232 {
233 ExpenseId = expense.Id,
234 UserId = splitParticipants[i],
235 Amount = splitAmt,
236 });
237 }
238 }
239
240 // Poll for trip 1
241 var poll1 = new TripPoll
242 {
243 TripId = trip1.Id, CreatedByUserId = admin.Id,
244 Question = "Where should we eat on the last night?",
245 AllowMultipleVotes = false, IsAnonymous = false,
246 };
247 context.TripPolls.Add(poll1);
248
249 var pollOpt1A = new TripPollOption { PollId = poll1.Id, Text = "Can Culleretes (oldest restaurant)", DisplayOrder = 0 };
250 var pollOpt1B = new TripPollOption { PollId = poll1.Id, Text = "El Xampanyet (tapas)", DisplayOrder = 1 };
251 var pollOpt1C = new TripPollOption { PollId = poll1.Id, Text = "Cerveceria Catalana", DisplayOrder = 2 };
252 context.TripPollOptions.AddRange(pollOpt1A, pollOpt1B, pollOpt1C);
253
254 context.TripPollVotes.Add(new TripPollVote { PollOptionId = pollOpt1B.Id, UserId = admin.Id });
255 context.TripPollVotes.Add(new TripPollVote { PollOptionId = pollOpt1A.Id, UserId = alice.Id });
256 context.TripPollVotes.Add(new TripPollVote { PollOptionId = pollOpt1B.Id, UserId = bob.Id });
257
258 // Wishlist for trip 1
259 context.TripWishlistItems.AddRange(
260 new TripWishlistItem
261 {
262 TripId = trip1.Id, AddedByUserId = alice.Id, Title = "Casa Batllo",
263 Description = "Gaudi's famous building on Passeig de Gracia",
264 Category = EWishlistCategory.Place, Priority = EWishlistPriority.MustDo,
265 EstimatedCost = 35m, Location = "Passeig de Gracia 43", DisplayOrder = 0
266 },
267 new TripWishlistItem
268 {
269 TripId = trip1.Id, AddedByUserId = bob.Id, Title = "Beach volleyball",
270 Description = "Play at Barceloneta beach in the morning",
271 Category = EWishlistCategory.Activity, Priority = EWishlistPriority.NiceToHave,
272 Location = "Barceloneta Beach", DisplayOrder = 1
273 },
274 new TripWishlistItem
275 {
276 TripId = trip1.Id, AddedByUserId = charlie.Id, Title = "Flamenco show",
277 Description = "Evening flamenco performance",
278 Category = EWishlistCategory.Activity, Priority = EWishlistPriority.MustDo,
279 EstimatedCost = 45m, DisplayOrder = 2
280 },
281 new TripWishlistItem
282 {
283 TripId = trip1.Id, AddedByUserId = admin.Id, Title = "La Paradeta seafood",
284 Description = "Fresh seafood market-style restaurant",
285 Category = EWishlistCategory.Restaurant, Priority = EWishlistPriority.Optional,
286 Location = "Carrer Comercial 7", DisplayOrder = 3
287 }
288 );
289
290 // ============================================================
291 // TRIP 2: London Business Trip (Settled, 3 participants)
292 // ============================================================
293 var trip2 = new Trip
294 {
295 Name = "London Business Trip",
296 Description = "Conference and team meetings in London",
297 Destination = "London, UK",
298 StartDate = new DateTime(2026, 3, 1, 0, 0, 0, DateTimeKind.Utc),
299 EndDate = new DateTime(2026, 3, 4, 0, 0, 0, DateTimeKind.Utc),
300 Status = ETripStatus.Settled,
301 DefaultCurrencyId = gbp.Id,
302 CreatedById = testUser.Id,
303 };
304 context.Trips.Add(trip2);
305
306 var tp2Test = new TripParticipant
307 {
308 TripId = trip2.Id, UserId = testUser.Id,
309 Role = EParticipantRole.Organizer, JoinedAt = DateTime.UtcNow.AddDays(-30), IsActive = true
310 };
311 var tp2Admin = new TripParticipant
312 {
313 TripId = trip2.Id, UserId = admin.Id,
314 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-29), IsActive = true
315 };
316 var tp2Diana = new TripParticipant
317 {
318 TripId = trip2.Id, UserId = diana.Id,
319 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-28), IsActive = true
320 };
321 context.TripParticipants.AddRange(tp2Test, tp2Admin, tp2Diana);
322
323 var trip2Participants = new[] { testUser.Id, admin.Id, diana.Id };
324
325 var expenses2 = new List<(string desc, decimal amount, Guid paidBy, DateTime date)>
326 {
327 ("Hotel (2 nights)", 340.00m, testUser.Id, DateTime.UtcNow.AddDays(-28)),
328 ("Heathrow Express", 75.00m, admin.Id, DateTime.UtcNow.AddDays(-28)),
329 ("Conference dinner", 185.00m, testUser.Id, DateTime.UtcNow.AddDays(-27)),
330 ("Uber rides", 48.00m, diana.Id, DateTime.UtcNow.AddDays(-27)),
331 ("Team lunch", 92.00m, admin.Id, DateTime.UtcNow.AddDays(-26)),
332 ("Coffee & snacks", 24.50m, diana.Id, DateTime.UtcNow.AddDays(-26)),
333 };
334
335 foreach (var (desc, amount, paidBy, date) in expenses2)
336 {
337 var expense = new Expense
338 {
339 TripId = trip2.Id, PaidByUserId = paidBy, Amount = amount,
340 Description = desc, ExpenseDate = date, CurrencyId = gbp.Id,
341 SplitMethod = ESplitMethod.EqualAll,
342 };
343 context.Expenses.Add(expense);
344
345 var count = trip2Participants.Length;
346 var baseAmt = Math.Floor(amount / count * 100) / 100;
347 var remainder = amount - baseAmt * count;
348 for (var i = 0; i < count; i++)
349 {
350 context.ExpenseSplits.Add(new ExpenseSplit
351 {
352 ExpenseId = expense.Id,
353 UserId = trip2Participants[i],
354 Amount = baseAmt + (i < (int)(remainder * 100) ? 0.01m : 0),
355 });
356 }
357 }
358
359 // Settlement plan for trip 2 (completed)
360 var plan2 = new SettlementPlan
361 {
362 TripId = trip2.Id, CreatedByUserId = testUser.Id,
363 TotalAmount = 255m, Status = ESettlementStatus.Completed,
364 CompletedAt = DateTime.UtcNow.AddDays(-20),
365 };
366 context.SettlementPlans.Add(plan2);
367
368 context.SettlementPayments.Add(new SettlementPayment
369 {
370 SettlementPlanId = plan2.Id, FromUserId = diana.Id, ToUserId = testUser.Id,
371 Amount = 130.50m, Status = EPaymentStatus.Confirmed,
372 MarkedPaidAt = DateTime.UtcNow.AddDays(-22), ConfirmedAt = DateTime.UtcNow.AddDays(-20),
373 });
374 context.SettlementPayments.Add(new SettlementPayment
375 {
376 SettlementPlanId = plan2.Id, FromUserId = diana.Id, ToUserId = admin.Id,
377 Amount = 28.00m, Status = EPaymentStatus.Confirmed,
378 MarkedPaidAt = DateTime.UtcNow.AddDays(-21), ConfirmedAt = DateTime.UtcNow.AddDays(-20),
379 });
380
381 // ============================================================
382 // TRIP 3: Summer Cabin (Active, 5 participants, with pending settlement)
383 // ============================================================
384 var trip3 = new Trip
385 {
386 Name = "Summer Cabin Getaway",
387 Description = "Relaxing weekend at a cabin by the lake",
388 Destination = "Otepaa, Estonia",
389 StartDate = new DateTime(2026, 5, 15, 0, 0, 0, DateTimeKind.Utc),
390 EndDate = new DateTime(2026, 5, 18, 0, 0, 0, DateTimeKind.Utc),
391 Status = ETripStatus.Active,
392 DefaultCurrencyId = eur.Id,
393 CreatedById = alice.Id,
394 };
395 context.Trips.Add(trip3);
396
397 var tp3Alice = new TripParticipant
398 {
399 TripId = trip3.Id, UserId = alice.Id,
400 Role = EParticipantRole.Organizer, JoinedAt = DateTime.UtcNow.AddDays(-3), IsActive = true
401 };
402 var tp3Bob = new TripParticipant
403 {
404 TripId = trip3.Id, UserId = bob.Id,
405 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-2), IsActive = true
406 };
407 var tp3Charlie = new TripParticipant
408 {
409 TripId = trip3.Id, UserId = charlie.Id,
410 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-2), IsActive = true
411 };
412 var tp3Diana = new TripParticipant
413 {
414 TripId = trip3.Id, UserId = diana.Id,
415 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-1), IsActive = true
416 };
417 var tp3Admin = new TripParticipant
418 {
419 TripId = trip3.Id, UserId = admin.Id,
420 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-1), IsActive = true
421 };
422 context.TripParticipants.AddRange(tp3Alice, tp3Bob, tp3Charlie, tp3Diana, tp3Admin);
423
424 var trip3Participants = new[] { alice.Id, bob.Id, charlie.Id, diana.Id, admin.Id };
425
426 var expenses3 = new List<(string desc, decimal amount, Guid paidBy, DateTime date)>
427 {
428 ("Cabin rental (3 nights)", 600.00m, alice.Id, DateTime.UtcNow.AddDays(-2)),
429 ("BBQ supplies and meat", 95.00m, bob.Id, DateTime.UtcNow.AddDays(-1)),
430 ("Firewood and charcoal", 25.00m, charlie.Id, DateTime.UtcNow.AddDays(-1)),
431 ("Drinks and beverages", 78.00m, diana.Id, DateTime.UtcNow),
432 ("Fishing gear rental", 40.00m, admin.Id, DateTime.UtcNow),
433 ("Breakfast groceries", 42.00m, alice.Id, DateTime.UtcNow),
434 ("Canoe rental (half day)", 60.00m, bob.Id, DateTime.UtcNow),
435 };
436
437 foreach (var (desc, amount, paidBy, date) in expenses3)
438 {
439 var expense = new Expense
440 {
441 TripId = trip3.Id, PaidByUserId = paidBy, Amount = amount,
442 Description = desc, ExpenseDate = date, CurrencyId = eur.Id,
443 SplitMethod = ESplitMethod.EqualAll,
444 };
445 context.Expenses.Add(expense);
446
447 var count = trip3Participants.Length;
448 var baseAmt = Math.Floor(amount / count * 100) / 100;
449 var remainder = amount - baseAmt * count;
450 for (var i = 0; i < count; i++)
451 {
452 context.ExpenseSplits.Add(new ExpenseSplit
453 {
454 ExpenseId = expense.Id,
455 UserId = trip3Participants[i],
456 Amount = baseAmt + (i < (int)(remainder * 100) ? 0.01m : 0),
457 });
458 }
459 }
460
461 // Settlement plan for trip 3 (in progress - some paid, some pending)
462 var plan3 = new SettlementPlan
463 {
464 TripId = trip3.Id, CreatedByUserId = alice.Id,
465 TotalAmount = 350m, Status = ESettlementStatus.InProgress,
466 };
467 context.SettlementPlans.Add(plan3);
468
469 context.SettlementPayments.Add(new SettlementPayment
470 {
471 SettlementPlanId = plan3.Id, FromUserId = charlie.Id, ToUserId = alice.Id,
472 Amount = 145.00m, Status = EPaymentStatus.MarkedPaid,
473 MarkedPaidAt = DateTime.UtcNow.AddHours(-2),
474 });
475 context.SettlementPayments.Add(new SettlementPayment
476 {
477 SettlementPlanId = plan3.Id, FromUserId = diana.Id, ToUserId = alice.Id,
478 Amount = 110.00m, Status = EPaymentStatus.Pending,
479 });
480 context.SettlementPayments.Add(new SettlementPayment
481 {
482 SettlementPlanId = plan3.Id, FromUserId = admin.Id, ToUserId = bob.Id,
483 Amount = 95.00m, Status = EPaymentStatus.Pending,
484 });
485
486 // Poll for trip 3
487 var poll3 = new TripPoll
488 {
489 TripId = trip3.Id, CreatedByUserId = alice.Id,
490 Question = "What activity for Saturday afternoon?",
491 AllowMultipleVotes = true, IsAnonymous = false,
492 };
493 context.TripPolls.Add(poll3);
494
495 var pollOpt3A = new TripPollOption { PollId = poll3.Id, Text = "Hiking to the viewpoint", DisplayOrder = 0 };
496 var pollOpt3B = new TripPollOption { PollId = poll3.Id, Text = "Fishing at the lake", DisplayOrder = 1 };
497 var pollOpt3C = new TripPollOption { PollId = poll3.Id, Text = "Board games at the cabin", DisplayOrder = 2 };
498 var pollOpt3D = new TripPollOption { PollId = poll3.Id, Text = "Cycling around the area", DisplayOrder = 3 };
499 context.TripPollOptions.AddRange(pollOpt3A, pollOpt3B, pollOpt3C, pollOpt3D);
500
501 context.TripPollVotes.AddRange(
502 new TripPollVote { PollOptionId = pollOpt3A.Id, UserId = alice.Id },
503 new TripPollVote { PollOptionId = pollOpt3B.Id, UserId = alice.Id },
504 new TripPollVote { PollOptionId = pollOpt3A.Id, UserId = bob.Id },
505 new TripPollVote { PollOptionId = pollOpt3C.Id, UserId = charlie.Id },
506 new TripPollVote { PollOptionId = pollOpt3B.Id, UserId = diana.Id },
507 new TripPollVote { PollOptionId = pollOpt3D.Id, UserId = admin.Id },
508 new TripPollVote { PollOptionId = pollOpt3A.Id, UserId = admin.Id }
509 );
510
511 // Wishlist for trip 3
512 context.TripWishlistItems.AddRange(
513 new TripWishlistItem
514 {
515 TripId = trip3.Id, AddedByUserId = bob.Id, Title = "Smoke sauna experience",
516 Description = "Traditional Estonian smoke sauna at the lakeside",
517 Category = EWishlistCategory.Activity, Priority = EWishlistPriority.MustDo,
518 EstimatedCost = 15m, DisplayOrder = 0,
519 },
520 new TripWishlistItem
521 {
522 TripId = trip3.Id, AddedByUserId = diana.Id, Title = "Visit Otepaa Adventure Park",
523 Description = "Rope courses and zip lines in the forest",
524 Category = EWishlistCategory.Activity, Priority = EWishlistPriority.NiceToHave,
525 EstimatedCost = 25m, Location = "Otepaa Adventure Park", DisplayOrder = 1,
526 },
527 new TripWishlistItem
528 {
529 TripId = trip3.Id, AddedByUserId = alice.Id, Title = "Puhajaarve beach",
530 Description = "Swimming and sunbathing at the sacred lake",
531 Category = EWishlistCategory.Place, Priority = EWishlistPriority.MustDo,
532 Location = "Puhajaarv", DisplayOrder = 2, IsCompleted = true, CompletedAt = DateTime.UtcNow.AddHours(-5),
533 }
534 );
535
536 // Invitation for trip 3
537 context.TripInvitations.Add(new TripInvitation
538 {
539 TripId = trip3.Id, InvitedByUserId = alice.Id,
540 Token = Guid.NewGuid().ToString("N"),
541 Status = EInvitationStatus.Pending,
542 ExpiresAt = DateTime.UtcNow.AddDays(7),
543 });
544
545 // ============================================================
546 // TRIP 4: New York City (Archived, completed)
547 // ============================================================
548 var trip4 = new Trip
549 {
550 Name = "NYC Adventure",
551 Description = "Week in New York City exploring Manhattan and Brooklyn",
552 Destination = "New York, USA",
553 StartDate = new DateTime(2025, 12, 20, 0, 0, 0, DateTimeKind.Utc),
554 EndDate = new DateTime(2025, 12, 27, 0, 0, 0, DateTimeKind.Utc),
555 Status = ETripStatus.Archived,
556 DefaultCurrencyId = usd.Id,
557 CreatedById = bob.Id,
558 };
559 context.Trips.Add(trip4);
560
561 context.TripParticipants.AddRange(
562 new TripParticipant
563 {
564 TripId = trip4.Id, UserId = bob.Id,
565 Role = EParticipantRole.Organizer, JoinedAt = DateTime.UtcNow.AddDays(-90), IsActive = true
566 },
567 new TripParticipant
568 {
569 TripId = trip4.Id, UserId = alice.Id,
570 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-89), IsActive = true
571 },
572 new TripParticipant
573 {
574 TripId = trip4.Id, UserId = diana.Id,
575 Role = EParticipantRole.Participant, JoinedAt = DateTime.UtcNow.AddDays(-88), IsActive = true
576 }
577 );
578
579 var trip4Participants = new[] { bob.Id, alice.Id, diana.Id };
580
581 var expenses4 = new List<(string desc, decimal amount, Guid paidBy, DateTime date)>
582 {
583 ("Hotel in Midtown (6 nights)", 1800.00m, bob.Id, new DateTime(2025, 12, 20, 12, 0, 0, DateTimeKind.Utc)),
584 ("Broadway show tickets", 450.00m, alice.Id, new DateTime(2025, 12, 21, 20, 0, 0, DateTimeKind.Utc)),
585 ("Statue of Liberty ferry", 63.00m, diana.Id, new DateTime(2025, 12, 22, 10, 0, 0, DateTimeKind.Utc)),
586 ("Central Park bike rental", 75.00m, bob.Id, new DateTime(2025, 12, 23, 14, 0, 0, DateTimeKind.Utc)),
587 ("Dinner in Little Italy", 195.00m, alice.Id, new DateTime(2025, 12, 23, 20, 0, 0, DateTimeKind.Utc)),
588 ("Brooklyn Bridge walk snacks", 28.00m, diana.Id, new DateTime(2025, 12, 24, 11, 0, 0, DateTimeKind.Utc)),
589 ("MoMA tickets", 75.00m, bob.Id, new DateTime(2025, 12, 25, 10, 0, 0, DateTimeKind.Utc)),
590 ("Times Square shopping", 220.00m, alice.Id, new DateTime(2025, 12, 26, 15, 0, 0, DateTimeKind.Utc)),
591 ("JFK taxi", 65.00m, diana.Id, new DateTime(2025, 12, 27, 8, 0, 0, DateTimeKind.Utc)),
592 };
593
594 foreach (var (desc, amount, paidBy, date) in expenses4)
595 {
596 var expense = new Expense
597 {
598 TripId = trip4.Id, PaidByUserId = paidBy, Amount = amount,
599 Description = desc, ExpenseDate = date, CurrencyId = usd.Id,
600 SplitMethod = ESplitMethod.EqualAll,
601 };
602 context.Expenses.Add(expense);
603
604 var count = trip4Participants.Length;
605 var baseAmt = Math.Floor(amount / count * 100) / 100;
606 var remainder = amount - baseAmt * count;
607 for (var i = 0; i < count; i++)
608 {
609 context.ExpenseSplits.Add(new ExpenseSplit
610 {
611 ExpenseId = expense.Id,
612 UserId = trip4Participants[i],
613 Amount = baseAmt + (i < (int)(remainder * 100) ? 0.01m : 0),
614 });
615 }
616 }
617
618 // Closed poll for trip 4
619 var poll4 = new TripPoll
620 {
621 TripId = trip4.Id, CreatedByUserId = bob.Id,
622 Question = "Best day of the trip?",
623 AllowMultipleVotes = false, IsAnonymous = false,
624 ClosedAt = new DateTime(2025, 12, 27, 12, 0, 0, DateTimeKind.Utc),
625 };
626 context.TripPolls.Add(poll4);
627
628 var pollOpt4A = new TripPollOption { PollId = poll4.Id, Text = "Broadway night", DisplayOrder = 0 };
629 var pollOpt4B = new TripPollOption { PollId = poll4.Id, Text = "Central Park day", DisplayOrder = 1 };
630 var pollOpt4C = new TripPollOption { PollId = poll4.Id, Text = "Brooklyn Bridge walk", DisplayOrder = 2 };
631 context.TripPollOptions.AddRange(pollOpt4A, pollOpt4B, pollOpt4C);
632
633 context.TripPollVotes.AddRange(
634 new TripPollVote { PollOptionId = pollOpt4A.Id, UserId = alice.Id },
635 new TripPollVote { PollOptionId = pollOpt4A.Id, UserId = diana.Id },
636 new TripPollVote { PollOptionId = pollOpt4B.Id, UserId = bob.Id }
637 );
638
639 context.SaveChanges();
640 }
641
642 }
643