2026-03-03 14:23:50 +01:00
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 > ( ) ;
2026-03-05 19:22:29 +01:00
await context . Database . MigrateAsync ( ) ;
using var transaction = await context . Database . BeginTransactionAsync ( ) ;
2026-03-18 09:08:45 +01:00
// Enable RLS on all tenant tables (Must be table owner, which 'workclub' is)
2026-03-05 19:22:29 +01:00
await context . Database . ExecuteSqlRawAsync ( @"
ALTER TABLE clubs ENABLE ROW LEVEL SECURITY;
ALTER TABLE clubs FORCE ROW LEVEL SECURITY;
ALTER TABLE members ENABLE ROW LEVEL SECURITY;
ALTER TABLE members FORCE ROW LEVEL SECURITY;
ALTER TABLE work_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE work_items FORCE ROW LEVEL SECURITY;
ALTER TABLE shifts ENABLE ROW LEVEL SECURITY;
ALTER TABLE shifts FORCE ROW LEVEL SECURITY;
ALTER TABLE shift_signups ENABLE ROW LEVEL SECURITY;
ALTER TABLE shift_signups FORCE ROW LEVEL SECURITY;
" ) ;
// Create tenant isolation policies (idempotent)
await context . Database . ExecuteSqlRawAsync ( @"
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='clubs' AND policyname='tenant_isolation_policy') THEN
CREATE POLICY tenant_isolation_policy ON clubs FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='members' AND policyname='tenant_isolation_policy') THEN
CREATE POLICY tenant_isolation_policy ON members FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='work_items' AND policyname='tenant_isolation_policy') THEN
CREATE POLICY tenant_isolation_policy ON work_items FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shifts' AND policyname='tenant_isolation_policy') THEN
CREATE POLICY tenant_isolation_policy ON shifts FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true));
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shift_signups' AND policyname='tenant_isolation_policy') THEN
CREATE POLICY tenant_isolation_policy ON shift_signups FOR ALL USING (""ShiftId"" IN (SELECT ""Id"" FROM shifts WHERE (""TenantId"")::text = current_setting('app.current_tenant_id', true)));
END IF;
END $$;
" ) ;
// Create admin bypass policies (idempotent)
await context . Database . ExecuteSqlRawAsync ( @"
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='clubs' AND policyname='bypass_rls_policy') THEN
CREATE POLICY bypass_rls_policy ON clubs FOR ALL TO app_admin USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='members' AND policyname='bypass_rls_policy') THEN
CREATE POLICY bypass_rls_policy ON members FOR ALL TO app_admin USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='work_items' AND policyname='bypass_rls_policy') THEN
CREATE POLICY bypass_rls_policy ON work_items FOR ALL TO app_admin USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shifts' AND policyname='bypass_rls_policy') THEN
CREATE POLICY bypass_rls_policy ON shifts FOR ALL TO app_admin USING (true);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shift_signups' AND policyname='bypass_rls_policy') THEN
CREATE POLICY bypass_rls_policy ON shift_signups FOR ALL TO app_admin USING (true);
END IF;
END $$;
" ) ;
await context . Database . ExecuteSqlRawAsync ( "SET LOCAL ROLE app_admin" ) ;
2026-03-03 14:23:50 +01:00
// 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 >
{
2026-03-18 09:08:45 +01:00
2026-03-03 14:23:50 +01:00
// 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 ( ) ;
}
2026-03-18 09:08:45 +01:00
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
AssigneeId = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
CreatedById = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = managerMember . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
2026-03-03 14:23:50 +01:00
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 ,
2026-03-18 09:08:45 +01:00
CreatedById = member1Members . First ( m = > m . ClubId = = cyclingClub . Id ) . Id ,
2026-03-03 14:23:50 +01:00
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 ( ) ;
}
}
2026-03-05 19:22:29 +01:00
await context . Database . ExecuteSqlRawAsync ( "RESET ROLE" ) ;
await transaction . CommitAsync ( ) ;
2026-03-03 14:23:50 +01:00
}
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 ( ) ;
}
}