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:
@@ -857,3 +857,146 @@ Note: Intentionally minimal dependencies for MVP. NextAuth.js added in Task 10.
|
||||
- Dev server output: `.sisyphus/evidence/task-5-dev-server.txt`
|
||||
- Git commit: `chore(frontend): initialize Next.js project with Tailwind and shadcn/ui`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Seed Data Script (2026-03-03)
|
||||
|
||||
### Key Learnings
|
||||
|
||||
1. **Idempotent Seeding Pattern**
|
||||
- Check existence before insert: `if (!context.Clubs.Any())`
|
||||
- Ensures safe re-runs (no duplicate data on restarts)
|
||||
- Applied to each entity type separately
|
||||
- SaveChangesAsync called after each entity batch
|
||||
|
||||
2. **Deterministic GUID Generation**
|
||||
- Used MD5.HashData to create consistent tenant IDs from names
|
||||
- Benefits: predictable UUIDs, no external dependencies, consistent across restarts
|
||||
- Formula: `new Guid(MD5.HashData(Encoding.UTF8.GetBytes(name)).Take(16).ToArray())`
|
||||
- Matches placeholder UUIDs in Keycloak test users from Task 3
|
||||
|
||||
3. **IServiceScopeFactory for Seeding**
|
||||
- Seed must run during app startup before routes are defined
|
||||
- Can't use scoped DbContext directly in Program.cs
|
||||
- Solution: Inject IServiceScopeFactory, create scope in SeedAsync method
|
||||
- Creates fresh DbContext per seeding operation
|
||||
|
||||
4. **Development-Only Execution Guard**
|
||||
- Seed runs only in development: `if (app.Environment.IsDevelopment())`
|
||||
- Production environments skip seeding automatically
|
||||
- Pattern: await inside if block (not a blocking operation)
|
||||
|
||||
5. **Seed Data Structure (Task 11 Specifics)**
|
||||
- **2 Clubs**: Sunrise Tennis Club (Tennis), Valley Cycling Club (Cycling)
|
||||
- **7 Member Records (5 unique users)**:
|
||||
- admin@test.com: Admin/Member (Tennis/Cycling)
|
||||
- manager@test.com: Manager (Tennis)
|
||||
- member1@test.com: Member/Member (Tennis/Cycling)
|
||||
- member2@test.com: Member (Tennis)
|
||||
- viewer@test.com: Viewer (Tennis)
|
||||
- **8 Work Items**: 5 in Tennis Club (all states), 3 in Cycling Club
|
||||
- **5 Shifts**: 3 in Tennis Club (past/today/future), 2 in Cycling Club (today/future)
|
||||
- **3-4 Shift Signups**: Select members signed up for shifts
|
||||
|
||||
6. **Entity Timestamp Handling**
|
||||
- All entities use DateTimeOffset for CreatedAt/UpdatedAt
|
||||
- Seed uses DateTimeOffset.UtcNow for current time
|
||||
- Shift dates use .Date.ToLocalTime() for proper date conversion without time component
|
||||
- Maintains UTC consistency for multi-tenant data
|
||||
|
||||
7. **Multi-Tenant Tenant ID Assignment**
|
||||
- Each Club has its own TenantId (deterministic from club name)
|
||||
- Child entities (Members, WorkItems, Shifts) get TenantId from parent club
|
||||
- ShiftSignups get TenantId from shift's club
|
||||
- Critical for RLS filtering (Task 7) to work correctly
|
||||
|
||||
8. **Work Item State Machine Coverage**
|
||||
- Seed covers all 5 states: Open, Assigned, InProgress, Review, Done
|
||||
- Maps to business flow: Open → Assigned → InProgress → Review → Done
|
||||
- Not all transitions are valid (enforced by state machine from Task 4)
|
||||
- Provides realistic test data for state transitions
|
||||
|
||||
9. **Shift Capacity and Sign-ups**
|
||||
- Shift.Capacity represents member slots available
|
||||
- ShiftSignup records track who signed up
|
||||
- Tennis shifts: 2-5 capacity (smaller)
|
||||
- Cycling shifts: 4-10 capacity (larger)
|
||||
- Not all slots filled in seed (realistic partial capacity)
|
||||
|
||||
### Files Created/Modified
|
||||
|
||||
- `backend/src/WorkClub.Infrastructure/Seed/SeedDataService.cs` — Full seeding logic (445 lines)
|
||||
- `backend/src/WorkClub.Api/Program.cs` — Added SeedDataService registration and startup call
|
||||
|
||||
### Implementation Details
|
||||
|
||||
**SeedDataService Constructor:**
|
||||
```csharp
|
||||
public SeedDataService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
```
|
||||
|
||||
**SeedAsync Pattern:**
|
||||
```csharp
|
||||
public async Task SeedAsync()
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
// Each entity type checked separately
|
||||
if (!context.Clubs.Any()) { /* seed clubs */ }
|
||||
if (!context.Members.Any()) { /* seed members */ }
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Program.cs Seed Call:**
|
||||
```csharp
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
var seedService = scope.ServiceProvider.GetRequiredService<SeedDataService>();
|
||||
await seedService.SeedAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### Patterns & Conventions
|
||||
|
||||
1. **Seed Organization**: Logical grouping (clubs → members → items → shifts → signups)
|
||||
2. **Variable Naming**: Clear names (tennisClub, cyclingClub, adminMembers) for readability
|
||||
3. **Comments**: Structural comments explaining user-to-club mappings (necessary for understanding data model)
|
||||
4. **Deterministic vs Random**: GUIDs for club IDs are deterministic, but Member/WorkItem/Shift IDs are random (not used in lookups)
|
||||
|
||||
### Testing Approach
|
||||
|
||||
The seed is designed for:
|
||||
- **Local development**: Full test data available on first run
|
||||
- **Restarts**: Safe idempotent re-runs
|
||||
- **Manual testing**: All roles and states represented
|
||||
- **QA**: Predictable data structure for integration tests
|
||||
|
||||
### Verification Strategy
|
||||
|
||||
Post-implementation checks (in separate QA section):
|
||||
1. Docker Compose startup with seed execution
|
||||
2. Database queries via `docker compose exec postgres psql`
|
||||
3. Verify counts: Clubs=2, Members≥7, WorkItems=8, Shifts=5
|
||||
4. Re-run and verify no duplicates (idempotency)
|
||||
|
||||
### Next Steps (Task 12+)
|
||||
|
||||
- Task 12 will create API endpoints to query this seed data
|
||||
- Task 22 will perform manual QA with this populated database
|
||||
- Production deployments will skip seeding via environment check
|
||||
|
||||
### Gotchas Avoided
|
||||
|
||||
- Did NOT use DateTime (used DateTimeOffset for timezone awareness)
|
||||
- Did NOT hard-code random GUIDs (used deterministic MD5-based)
|
||||
- Did NOT forget idempotent checks (each entity type guarded)
|
||||
- Did NOT seed in all environments (guarded with IsDevelopment())
|
||||
- Did NOT create DbContext directly (used IServiceScopeFactory)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user