feat(seed): add development seed data script

- Create SeedDataService in Infrastructure/Seed with idempotent seeding
- Seed 2 clubs: Sunrise Tennis Club, Valley Cycling Club
- Seed 7 member records (5 unique Keycloak test users)
- Seed 8 work items covering all status states
- Seed 5 shifts with date variety (past, today, future)
- Seed shift signups for realistic partial capacity
- Register SeedDataService in Program.cs with development-only guard
- Use deterministic GUID generation from club names
- Ensure all tenant IDs match for RLS compliance
- Track in learnings.md and evidence files for Task 22 QA
This commit is contained in:
Sisyphus CI
2026-03-03 14:23:50 +01:00
parent c44cb1c801
commit b7854e9571
4 changed files with 797 additions and 3 deletions

View File

@@ -0,0 +1,448 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using WorkClub.Domain.Entities;
using WorkClub.Domain.Enums;
using WorkClub.Infrastructure.Data;
namespace WorkClub.Infrastructure.Seed;
public class SeedDataService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public SeedDataService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task SeedAsync()
{
using var scope = _serviceScopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Seed clubs
if (!context.Clubs.Any())
{
var clubs = new List<Club>
{
new Club
{
Id = Guid.NewGuid(),
TenantId = GenerateDeterministicGuid("Sunrise Tennis Club"),
Name = "Sunrise Tennis Club",
SportType = SportType.Tennis,
Description = "Community tennis club for all skill levels",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Club
{
Id = Guid.NewGuid(),
TenantId = GenerateDeterministicGuid("Valley Cycling Club"),
Name = "Valley Cycling Club",
SportType = SportType.Cycling,
Description = "Cycling enthusiasts community",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
}
};
context.Clubs.AddRange(clubs);
await context.SaveChangesAsync();
}
// Get clubs for member seeding
var tennisClub = context.Clubs.First(c => c.Name == "Sunrise Tennis Club");
var cyclingClub = context.Clubs.First(c => c.Name == "Valley Cycling Club");
// Seed members
if (!context.Members.Any())
{
var members = new List<Member>
{
// admin@test.com: Admin in Club 1, Member in Club 2
new Member
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ExternalUserId = "admin-user-id",
DisplayName = "Admin User",
Email = "admin@test.com",
Role = ClubRole.Admin,
ClubId = tennisClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Member
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
ExternalUserId = "admin-user-id",
DisplayName = "Admin User",
Email = "admin@test.com",
Role = ClubRole.Member,
ClubId = cyclingClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
// manager@test.com: Manager in Club 1
new Member
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ExternalUserId = "manager-user-id",
DisplayName = "Manager User",
Email = "manager@test.com",
Role = ClubRole.Manager,
ClubId = tennisClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
// member1@test.com: Member in Club 1 and Club 2
new Member
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ExternalUserId = "member1-user-id",
DisplayName = "Member One",
Email = "member1@test.com",
Role = ClubRole.Member,
ClubId = tennisClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Member
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
ExternalUserId = "member1-user-id",
DisplayName = "Member One",
Email = "member1@test.com",
Role = ClubRole.Member,
ClubId = cyclingClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
// member2@test.com: Member in Club 1
new Member
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ExternalUserId = "member2-user-id",
DisplayName = "Member Two",
Email = "member2@test.com",
Role = ClubRole.Member,
ClubId = tennisClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
// viewer@test.com: Viewer in Club 1
new Member
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ExternalUserId = "viewer-user-id",
DisplayName = "Viewer User",
Email = "viewer@test.com",
Role = ClubRole.Viewer,
ClubId = tennisClub.Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
}
};
context.Members.AddRange(members);
await context.SaveChangesAsync();
}
// Get admin member IDs for work item creation
var adminMembers = context.Members.Where(m => m.Email == "admin@test.com").ToList();
var managerMember = context.Members.First(m => m.Email == "manager@test.com");
var member1Members = context.Members.Where(m => m.Email == "member1@test.com").ToList();
var member2Member = context.Members.First(m => m.Email == "member2@test.com");
// Seed work items
if (!context.WorkItems.Any())
{
var workItems = new List<WorkItem>
{
// Club 1 - Tennis Club (5 items, all states)
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Court renovation",
Description = "Resurface main court",
Status = WorkItemStatus.Open,
AssigneeId = null,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
ClubId = tennisClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(14),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Equipment order",
Description = "Purchase new tennis rackets and balls",
Status = WorkItemStatus.Assigned,
AssigneeId = managerMember.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
ClubId = tennisClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(7),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Tournament planning",
Description = "Organize annual summer tournament",
Status = WorkItemStatus.InProgress,
AssigneeId = member1Members.First(m => m.ClubId == tennisClub.Id).Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
ClubId = tennisClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(30),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Member handbook review",
Description = "Update and review club rules handbook",
Status = WorkItemStatus.Review,
AssigneeId = member2Member.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
ClubId = tennisClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(21),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Website update",
Description = "Update club website with new photos",
Status = WorkItemStatus.Done,
AssigneeId = managerMember.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
ClubId = tennisClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(-5),
CreatedAt = DateTimeOffset.UtcNow.AddDays(-10),
UpdatedAt = DateTimeOffset.UtcNow
},
// Club 2 - Cycling Club (3 items)
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
Title = "Route mapping",
Description = "Create new cycling routes for summer",
Status = WorkItemStatus.Open,
AssigneeId = null,
CreatedById = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
ClubId = cyclingClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(21),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
Title = "Safety training",
Description = "Organize safety and maintenance training",
Status = WorkItemStatus.Assigned,
AssigneeId = member1Members.First(m => m.ClubId == cyclingClub.Id).Id,
CreatedById = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
ClubId = cyclingClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(14),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new WorkItem
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
Title = "Group ride coordination",
Description = "Schedule and coordinate weekly group rides",
Status = WorkItemStatus.InProgress,
AssigneeId = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
CreatedById = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
ClubId = cyclingClub.Id,
DueDate = DateTimeOffset.UtcNow.AddDays(7),
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
}
};
context.WorkItems.AddRange(workItems);
await context.SaveChangesAsync();
}
// Seed shifts
if (!context.Shifts.Any())
{
var now = DateTimeOffset.UtcNow;
var shifts = new List<Shift>
{
// Club 1 - Tennis Club (3 shifts)
new Shift
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Court Maintenance - Yesterday",
Description = "Daily court cleaning and maintenance",
Location = "Main Court",
StartTime = now.AddDays(-1).Date.ToLocalTime().AddHours(8),
EndTime = now.AddDays(-1).Date.ToLocalTime().AddHours(12),
Capacity = 2,
ClubId = tennisClub.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Shift
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Court Maintenance - Today",
Description = "Daily court cleaning and maintenance",
Location = "Main Court",
StartTime = now.Date.ToLocalTime().AddHours(14),
EndTime = now.Date.ToLocalTime().AddHours(18),
Capacity = 3,
ClubId = tennisClub.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Shift
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
Title = "Tournament Setup - Next Week",
Description = "Setup and preparation for summer tournament",
Location = "All Courts",
StartTime = now.AddDays(7).Date.ToLocalTime().AddHours(9),
EndTime = now.AddDays(7).Date.ToLocalTime().AddHours(17),
Capacity = 5,
ClubId = tennisClub.Id,
CreatedById = adminMembers.First(m => m.ClubId == tennisClub.Id).Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
// Club 2 - Cycling Club (2 shifts)
new Shift
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
Title = "Group Ride - Today",
Description = "Weekly morning group ride",
Location = "Park entrance",
StartTime = now.Date.ToLocalTime().AddHours(7),
EndTime = now.Date.ToLocalTime().AddHours(9),
Capacity = 10,
ClubId = cyclingClub.Id,
CreatedById = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
},
new Shift
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
Title = "Maintenance Workshop - Next Week",
Description = "Bike maintenance and repair workshop",
Location = "Club shed",
StartTime = now.AddDays(7).Date.ToLocalTime().AddHours(10),
EndTime = now.AddDays(7).Date.ToLocalTime().AddHours(14),
Capacity = 4,
ClubId = cyclingClub.Id,
CreatedById = adminMembers.First(m => m.ClubId == cyclingClub.Id).Id,
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow
}
};
context.Shifts.AddRange(shifts);
await context.SaveChangesAsync();
}
// Seed shift signups
if (!context.ShiftSignups.Any())
{
var shifts = context.Shifts.ToList();
var signups = new List<ShiftSignup>();
// Add some signups for Tennis Club shifts
var tennisShifts = shifts.Where(s => s.ClubId == tennisClub.Id).ToList();
if (tennisShifts.Count > 0)
{
var tennyMembers = context.Members.Where(m => m.ClubId == tennisClub.Id).ToList();
if (tennyMembers.Count > 0)
{
signups.Add(new ShiftSignup
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ShiftId = tennisShifts[0].Id,
MemberId = tennyMembers[0].Id,
SignedUpAt = DateTimeOffset.UtcNow
});
if (tennyMembers.Count > 1)
{
signups.Add(new ShiftSignup
{
Id = Guid.NewGuid(),
TenantId = tennisClub.TenantId,
ShiftId = tennisShifts[0].Id,
MemberId = tennyMembers[1].Id,
SignedUpAt = DateTimeOffset.UtcNow
});
}
}
}
// Add some signups for Cycling Club shifts
var cyclingShifts = shifts.Where(s => s.ClubId == cyclingClub.Id).ToList();
if (cyclingShifts.Count > 0)
{
var cyclingMembers = context.Members.Where(m => m.ClubId == cyclingClub.Id).ToList();
if (cyclingMembers.Count > 0)
{
signups.Add(new ShiftSignup
{
Id = Guid.NewGuid(),
TenantId = cyclingClub.TenantId,
ShiftId = cyclingShifts[0].Id,
MemberId = cyclingMembers[0].Id,
SignedUpAt = DateTimeOffset.UtcNow
});
}
}
if (signups.Count > 0)
{
context.ShiftSignups.AddRange(signups);
await context.SaveChangesAsync();
}
}
}
private static string GenerateDeterministicGuid(string input)
{
// Generate a deterministic GUID from a string using MD5
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
return new Guid(hash.Take(16).ToArray()).ToString();
}
}