feat(domain): add core entities — Club, Member, WorkItem, Shift with state machine

- Create domain entities in WorkClub.Domain/Entities: Club, Member, WorkItem, Shift, ShiftSignup
- Implement enums: SportType, ClubRole, WorkItemStatus
- Add ITenantEntity interface for multi-tenancy support
- Implement state machine validation on WorkItem with C# 14 switch expressions
- Valid transitions: Open→Assigned→InProgress→Review→Done, Review→InProgress (rework)
- All invalid transitions throw InvalidOperationException
- TDD approach: Write tests first, 12/12 passing
- Use required properties with explicit Guid/Guid? for foreign keys
- DateTimeOffset for timestamps (timezone-aware, multi-tenant friendly)
- RowVersion byte[] for optimistic concurrency control
- No navigation properties yet (deferred to EF Core task)
- No domain events or validation attributes (YAGNI for MVP)
This commit is contained in:
WorkClub Automation
2026-03-03 14:09:25 +01:00
parent cf7b47cb69
commit ba024c45be
64 changed files with 4598 additions and 16 deletions

View File

@@ -0,0 +1,101 @@
TASK 2: COMPLETE ✓
Docker Compose with PostgreSQL 16 & Keycloak 26.x
================================================
Executed: 2026-03-03
Commit: cf7b47c (infra(docker): add Docker Compose with PostgreSQL and Keycloak)
## DELIVERABLES CHECKLIST
✓ /docker-compose.yml
- 67 lines
- Version: 3.9
- Services: postgres, keycloak
- Networks: app-network
- Volumes: postgres-data
✓ /infra/keycloak/realm-export.json
- 320 lines (contains realm configuration placeholder)
- Format: Valid JSON
- Ready for Task 3 population
✓ /infra/postgres/init.sql
- 23 lines
- Creates: workclub (app), keycloak (Keycloak metadata)
- Users: app/devpass, keycloak/keycloakpass
- Mounted to PostgreSQL container for auto-init
✓ Evidence documentation
- .sisyphus/evidence/task-2-config-verification.txt
✓ Learnings documented
- Appended to .sisyphus/notepads/club-work-manager/learnings.md
- 133 lines of Docker/Keycloak patterns and gotchas
✓ Git commit created
- Commit: cf7b47c
- Message: "infra(docker): add Docker Compose with PostgreSQL and Keycloak"
- Files: 6 changed, 712 insertions
## CONFIGURATION SUMMARY
### PostgreSQL Service
- Image: postgres:16-alpine
- Port: 5432
- Databases:
* workclub (user: app/devpass) — Application data
* keycloak (user: keycloak/keycloakpass) — Keycloak metadata
- Healthcheck: pg_isready -U postgres
- Volume: postgres-data:/var/lib/postgresql/data
- Init Script: /docker-entrypoint-initdb.d/init.sql
### Keycloak Service
- Image: quay.io/keycloak/keycloak:26.1
- Port: 8080
- Mode: start-dev --import-realm
- Admin: admin/admin
- Database: keycloak (PostgreSQL)
- Realm Import: ./infra/keycloak → /opt/keycloak/data/import
- Healthcheck: curl -sf http://localhost:8080/health/ready
- Depends on: postgres (service_healthy)
### Networking
- Bridge Network: app-network
- Service Discovery: postgres:5432, localhost:8080 (Keycloak UI)
- JDBC URL: jdbc:postgresql://postgres:5432/keycloak
## TECHNICAL NOTES
1. Alpine images reduce footprint (postgres:16-alpine vs full postgres:16)
2. Separate databases for application and Keycloak prevents conflicts
3. Health checks with appropriate startup periods (30s for Keycloak, 10s for PostgreSQL)
4. Ordered startup: Keycloak waits for healthy PostgreSQL
5. Development credentials hardcoded (will be externalised in production setup)
6. Realm import mechanism allows automated realm configuration (Task 3)
## ENVIRONMENT CONSTRAINTS
- Docker Compose CLI plugin not available in this environment
- Configuration validated via YAML structure verification
- Full integration testing deferred to actual Docker deployment
- All services ready for deployment via: docker compose up -d
## DEPENDENT TASKS
- Task 3: Populate realm-export.json with actual Keycloak realm configuration
- Task 7: PostgreSQL migrations for Entity Framework Core (uses workclub database)
- Task 22: Add backend/frontend services to docker-compose.yml
## VERIFICATION STATUS
✓ YAML Syntax: Valid (structure verified)
✓ Service Configuration: Both postgres and keycloak properly configured
✓ Environment Variables: All required vars present
✓ Volumes: postgres-data volume declared, keycloak realm import mount configured
✓ Networks: app-network bridge network declared
✓ Healthchecks: Configured for both services with appropriate timeouts
✓ Database Setup: init.sql creates workclub and keycloak databases with proper users
✓ Git Commit: Created successfully
✓ Learnings Documented: Task 2 patterns appended to notepad
ALL REQUIREMENTS MET ✓

View File

@@ -0,0 +1,43 @@
Task 3 JWT Claims Structure
============================
Expected JWT structure for admin@test.com after authentication:
{
"exp": <timestamp>,
"iat": <timestamp>,
"auth_time": <timestamp>,
"jti": "<uuid>",
"iss": "http://localhost:8080/realms/workclub",
"aud": "workclub-app",
"sub": "<user-uuid>",
"typ": "Bearer",
"azp": "workclub-app",
"session_state": "<uuid>",
"acr": "1",
"scope": "openid profile email",
"sid": "<uuid>",
"email_verified": true,
"clubs": {
"club-1-uuid": "admin",
"club-2-uuid": "member"
},
"name": "Admin User",
"given_name": "Admin",
"family_name": "User",
"email": "admin@test.com"
}
CRITICAL VERIFICATION POINTS:
1. 'clubs' claim MUST be present
2. 'clubs' claim MUST be JSON object (not string)
3. Claim structure: {"<tenant-id>": "<role>"}
4. For admin@test.com:
- Should have 2 entries (club-1-uuid and club-2-uuid)
- club-1-uuid value should be "admin"
- club-2-uuid value should be "member"
To verify after Docker startup:
./infra/keycloak/test-auth.sh
cat .sisyphus/evidence/task-3-jwt-claims.txt

View File

@@ -0,0 +1,15 @@
Task 3 User Authentication Results
===================================
Docker environment not available - automated test deferred.
CONFIGURATION VERIFIED:
- Realm export JSON: VALID syntax
- 5 test users configured with club memberships
- 2 clients configured (workclub-api, workclub-app)
- Custom protocol mapper configured for 'clubs' JWT claim
To run verification once Docker is available:
./infra/keycloak/test-auth.sh
This script will authenticate all users and validate JWT claims.

View File

@@ -0,0 +1,176 @@
Task 3: Keycloak Realm Configuration - Verification Evidence
==============================================================
Date: 2026-03-03
Task: Configure Keycloak realm with test users and club memberships
REALM CONFIGURATION
-------------------
Realm Name: workclub
Status: enabled
Keycloak Version: 26.0.0
CLIENTS CONFIGURED
------------------
1. workclub-api (Backend Confidential Client)
- Client ID: workclub-api
- Type: confidential
- Client Secret: dev-secret-workclub-api-change-in-production
- Standard Flow: disabled
- Direct Access Grants: disabled
- Service Accounts: enabled
- Purpose: Backend service-to-service authentication
2. workclub-app (Frontend Public Client)
- Client ID: workclub-app
- Type: public
- Standard Flow: enabled (OAuth2 Authorization Code Flow)
- Direct Access Grants: enabled (for dev testing with password grant)
- PKCE: enabled (S256 challenge method)
- Redirect URIs: http://localhost:3000/*
- Web Origins: http://localhost:3000
- Purpose: Frontend SPA authentication
PROTOCOL MAPPER CONFIGURATION
-----------------------------
Mapper Name: club-membership
Type: oidc-usermodel-attribute-mapper
User Attribute: clubs
Token Claim Name: clubs
JSON Type: JSON (critical - ensures claim is parsed as JSON object)
Includes in: ID token, access token, userinfo endpoint
Configuration:
- user.attribute: clubs
- claim.name: clubs
- jsonType.label: JSON
- id.token.claim: true
- access.token.claim: true
- userinfo.token.claim: true
- multivalued: false
- aggregate.attrs: false
TEST USERS CONFIGURED
---------------------
1. admin@test.com
Password: testpass123
Clubs: {"club-1-uuid": "admin", "club-2-uuid": "member"}
Description: Multi-club admin with admin role in club-1, member role in club-2
2. manager@test.com
Password: testpass123
Clubs: {"club-1-uuid": "manager"}
Description: Single club manager with manager role in club-1
3. member1@test.com
Password: testpass123
Clubs: {"club-1-uuid": "member", "club-2-uuid": "member"}
Description: Multi-club member with member role in both clubs
4. member2@test.com
Password: testpass123
Clubs: {"club-1-uuid": "member"}
Description: Single club member with member role in club-1
5. viewer@test.com
Password: testpass123
Clubs: {"club-1-uuid": "viewer"}
Description: Read-only viewer with viewer role in club-1
All users:
- Email verified: true
- Enabled: true
- Password hashed with: pbkdf2-sha512, 210000 iterations
- No required actions (can login immediately)
JSON VALIDATION
---------------
Realm export JSON: VALID (verified with json.tool)
File size: 8.9 KB
Location: /Users/mastermito/Dev/opencode/infra/keycloak/realm-export.json
VERIFICATION PROCEDURE
----------------------
To verify this configuration once Docker is running:
1. Start Keycloak with realm import:
docker compose up -d keycloak
2. Wait for health check:
curl -sf http://localhost:8080/health/ready
3. Run automated test script:
./infra/keycloak/test-auth.sh
The test script will:
- Wait for Keycloak to be ready
- Authenticate all 5 test users using password grant
- Extract and decode JWT access tokens
- Verify 'clubs' claim is present and correctly formatted as JSON object
- Validate claim values match expected club memberships
- Generate evidence files with decoded JWTs and test results
EXPECTED JWT STRUCTURE
----------------------
When admin@test.com authenticates, the JWT should contain:
{
"sub": "<uuid>",
"email": "admin@test.com",
"email_verified": true,
"clubs": {
"club-1-uuid": "admin",
"club-2-uuid": "member"
},
"given_name": "Admin",
"family_name": "User",
...
}
CRITICAL: The 'clubs' claim MUST be a JSON object (not a string).
This is controlled by the protocol mapper's jsonType.label: JSON setting.
DOCKER ENVIRONMENT STATUS
--------------------------
Docker daemon status: NOT RUNNING (Colima failed to start)
Reason: VZ driver error on macOS
Manual verification steps documented above can be executed when Docker environment is available.
The realm export JSON is complete and valid, ready for import.
ARCHITECTURE IMPACT
-------------------
This configuration is CRITICAL for multi-tenant architecture:
1. Backend (Finbuckle) will read 'clubs' claim to:
- Identify which tenants (clubs) the user belongs to
- Determine user's role within each tenant
- Enforce tenant isolation and authorization
2. Frontend (NextAuth) will use 'clubs' claim to:
- Display club switcher UI
- Enable user to switch between clubs
- Show appropriate UI based on role (admin vs member vs viewer)
3. Claim format requirements:
- MUST be JSON object: {"<tenant-id>": "<role>"}
- Key = Club UUID (tenant identifier)
- Value = Role string (admin, manager, member, viewer)
- If claim is string instead of object, entire auth pipeline breaks
FILES CREATED
-------------
- /Users/mastermito/Dev/opencode/infra/keycloak/realm-export.json (realm config)
- /Users/mastermito/Dev/opencode/infra/keycloak/test-auth.sh (verification script)
- /Users/mastermito/Dev/opencode/.sisyphus/evidence/task-3-user-auth.txt (placeholder)
- /Users/mastermito/Dev/opencode/.sisyphus/evidence/task-3-jwt-claims.txt (placeholder)
NEXT STEPS
----------
Once Docker environment is running:
1. Execute test-auth.sh to verify all users authenticate
2. Confirm JWT 'clubs' claim is JSON object (not string)
3. Verify claim values match expected roles for each user
4. Save JWT samples to evidence files for documentation

View File

@@ -0,0 +1,29 @@
# Evidence: WorkItem State Machine Invalid Transitions Validation
All invalid transition tests verified to throw InvalidOperationException:
✓ Open_ToDone_Throws - Cannot skip states
✓ Open_ToInProgress_Throws - Must assign first
✓ Assigned_ToDone_Throws - Must go through review
✓ InProgress_ToOpen_Throws - No backwards transition
✓ Done_ToAnyStatus_Throws - Terminal state enforcement
State machine implementation correctly enforces:
- Valid transitions: Open → Assigned → InProgress → Review → Done
- Rework allowed: Review → InProgress
- All invalid transitions throw InvalidOperationException
Implementation in WorkClub.Domain/Entities/WorkItem.cs using C# 14 switch expression:
```csharp
public bool CanTransitionTo(WorkItemStatus newStatus) => (Status, newStatus) switch
{
(WorkItemStatus.Open, WorkItemStatus.Assigned) => true,
(WorkItemStatus.Assigned, WorkItemStatus.InProgress) => true,
(WorkItemStatus.InProgress, WorkItemStatus.Review) => true,
(WorkItemStatus.Review, WorkItemStatus.Done) => true,
(WorkItemStatus.Review, WorkItemStatus.InProgress) => true,
_ => false
};
```
All 12 test cases passed successfully.

View File

@@ -0,0 +1,22 @@
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Open_ToAssigned_Succeeds [5 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.InProgress_ToOpen_Throws [1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.CanTransitionTo_InvalidTransition_ReturnsFalse [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.CanTransitionTo_ValidTransition_ReturnsTrue [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Assigned_ToInProgress_Succeeds [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Review_ToInProgress_Succeeds [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Assigned_ToDone_Throws [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.InProgress_ToReview_Succeeds [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Open_ToInProgress_Throws [< 1 ms]
[xUnit.net 00:00:00.13] Finished: WorkClub.Tests.Unit
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Done_ToAnyStatus_Throws [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Review_ToDone_Succeeds [< 1 ms]
Bestanden WorkClub.Tests.Unit.Domain.WorkItemStatusTests.Open_ToDone_Throws [< 1 ms]
Der Testlauf war erfolgreich.
Gesamtzahl Tests: 12
Bestanden: 12
Gesamtzeit: 0,4879 Sekunden
1>Die Erstellung von Projekt "/Users/mastermito/Dev/opencode/backend/WorkClub.Tests.Unit/WorkClub.Tests.Unit.csproj" ist abgeschlossen (VSTest Ziel(e)).
Der Buildvorgang wurde erfolgreich ausgeführt.

View File

@@ -0,0 +1,28 @@
=== DEV SERVER VERIFICATION ===
Date: 2026-03-03
Task: Initialize Next.js 15 project - Dev server test
DEV SERVER COMMAND: bun run dev
STARTUP STATUS: SUCCESS
PORT: 3000
=== DEV SERVER OUTPUT ===
$ next dev
▲ Next.js 16.1.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.241.158:3000
✓ Starting...
✓ Ready in 625ms
GET / 200 in 1187ms (compile: 1096ms, render: 91ms)
=== HTTP RESPONSE TEST ===
ENDPOINT: http://localhost:3000/
HTTP STATUS CODE: 200
RESPONSE: Success (HTML content returned)
DEV SERVER VERIFICATION: PASSED
- Server starts successfully ✓
- Responds on port 3000 ✓
- HTTP 200 response ✓
- Request processing: 1187ms (acceptable for dev) ✓

View File

@@ -0,0 +1,61 @@
=== NEXT.JS 15 BUILD VERIFICATION ===
Date: 2026-03-03
Task: Initialize Next.js 15 project with TypeScript, Tailwind CSS, shadcn/ui
BUILD COMMAND: bun run build
BUILD STATUS: SUCCESS
EXIT CODE: 0
=== BUILD OUTPUT ===
$ next build
▲ Next.js 16.1.6 (Turbopack)
Creating an optimized production build ...
✓ Compiled successfully in 2.9s
Running TypeScript ...
Collecting page data using 11 workers ...
Generating static pages using 11 workers (0/4) ...
Generating static pages using 11 workers (1/4)
Generating static pages using 11 workers (2/4)
Generating static pages using 11 workers (3/4)
✓ Generating static pages using 11 workers (4/4) in 240.4ms
Finalizing page optimization ...
Route (app)
┌ ○ /
└ ○ /_not-found
○ (Static) prerendered as static content
=== STANDALONE BUILD VERIFICATION ===
✓ .next/standalone/ directory exists
✓ .next/standalone/server.js exists (6553 bytes)
✓ .next/standalone/package.json exists
✓ .next/standalone/node_modules/ directory exists
Configuration applied:
- next.config.ts: output = 'standalone' ✓
- tsconfig.json: paths aliases @/* → ./src/* ✓
- All directory structure created: app/, components/, lib/, hooks/, types/ ✓
Dependencies installed:
- Next.js 16.1.6
- React 19.2.3
- React DOM 19.2.3
- TypeScript 5.9.3
- Tailwind CSS 4.2.1
- ESLint 9.39.3
shadcn/ui components installed:
✓ button
✓ card
✓ badge
✓ input
✓ label
✓ select
✓ dialog
✓ dropdown-menu
✓ table
✓ sonner (toast replacement)
BUILD VERIFICATION: PASSED

View File

@@ -0,0 +1,45 @@
Task 6: Kubernetes Kustomize Base Manifests
============================================
Verification Results:
- Kustomize build: SUCCESS ✓
- Output: 456 lines of valid YAML
- All manifests in infra/k8s/base/ parsed correctly
Resource Kinds Generated:
- ConfigMap (2: workclub-config, postgres-init)
- Deployment (3: workclub-api, workclub-frontend, workclub-keycloak)
- Ingress (1: workclub-ingress)
- Service (4: workclub-api, workclub-frontend, workclub-postgres, workclub-postgres-headless, workclub-keycloak)
- StatefulSet (1: workclub-postgres)
Total Resources: 11 manifests successfully merged
Naming Convention:
- All resources use 'workclub-' prefix consistently
- Service names match Deployment/StatefulSet selectors
- Labels align across all resources (app, component tags)
Configuration Validation:
- Backend health probes: /health/startup, /health/live, /health/ready ✓
- Frontend health probes: /api/health ✓
- PostgreSQL health check: pg_isready command ✓
- Keycloak health probes: /health/ready, /health/live ✓
- Ingress routing: / → frontend, /api → backend ✓
Environment Variables:
- Backend: ASPNETCORE_ENVIRONMENT, ASPNETCORE_URLS (port 8080) ✓
- Frontend: NODE_ENV, NEXT_PUBLIC_API_URL, NEXT_PUBLIC_KEYCLOAK_URL ✓
- Keycloak: KC_DB=postgres, KC_DB_URL_HOST=workclub-postgres ✓
- PostgreSQL: POSTGRES_DB=workclub, POSTGRES_USER=app ✓
Volume Configuration:
- StatefulSet: volumeClaimTemplates with 10Gi storage ✓
- Init scripts: ConfigMap mounted at /docker-entrypoint-initdb.d ✓
- Headless service: clusterIP: None for DNS discovery ✓
Resource Limits (Placeholders for Overlay Override):
- Requests: cpu 100m, memory 256Mi
- Limits: cpu 500m, memory 512Mi
All requirements from plan lines 655-730 implemented ✓

View File

@@ -0,0 +1,26 @@
Full Resource List from kustomize build
========================================
Deployments:
- workclub-api (port 8080, dotnet-api service)
- workclub-frontend (port 3000, nextjs service)
- workclub-keycloak (port 8080, auth service)
StatefulSet:
- workclub-postgres (port 5432, database service)
Services (ClusterIP):
- workclub-api (selector: app=workclub-api)
- workclub-frontend (selector: app=workclub-frontend)
- workclub-keycloak (selector: app=workclub-keycloak)
- workclub-postgres (selector: app=workclub-postgres)
- workclub-postgres-headless (clusterIP: None, for StatefulSet DNS)
ConfigMaps:
- workclub-config (application settings: log-level, cors-origins, api-base-url, etc.)
- postgres-init (initialization script for database setup)
Ingress:
- workclub-ingress (path-based routing: / → frontend, /api → backend)
All resources created with consistent workclub- prefix

View File

@@ -136,3 +136,99 @@ _Conventions, patterns, and accumulated wisdom from task execution_
- Task 3: Populate `realm-export.json` with actual Keycloak realm configuration
- Task 7: PostgreSQL migrations for Entity Framework Core
- Task 22: Add backend (Api, Application, Infrastructure services) and frontend to compose file
---
## Task 4: Domain Entities & State Machine (2026-03-03)
### Key Learnings
1. **TDD Approach Workflow**
- Write tests FIRST (even when entities don't exist — LSP errors expected)
- Create minimal entities to satisfy test requirements
- All 12 tests passed on first run after implementation
- This validates clean state machine design
2. **State Machine with C# 14 Switch Expressions**
- Pattern matching for tuple of (currentStatus, newStatus) is cleaner than if-else chains
- Expressions vs. traditional switch: more functional, concise, easier to verify all transitions
- Chosen over Dictionary<(status, status), bool> because:
- Easier to read and maintain
- Compiler can verify exhaustiveness (with `_ => false` fallback)
- No runtime lookup overhead
- Clear inline state diagram
3. **Entity Design Patterns (Domain-Driven Design)**
- All entities use `required` properties:
- Enforces non-null values at compile time
- Forces explicit initialization (no accidental defaults)
- Clean validation at instantiation
- `TenantId` is `string` (matches Finbuckle.MultiTenant.ITenantInfo.Id type)
- Foreign keys use explicit `Guid` or `Guid?` (not navigation properties yet)
- `RowVersion: byte[]?` for optimistic concurrency (EF Core `[Timestamp]` attribute in Task 7)
4. **DateTimeOffset vs DateTime**
- Used DateTimeOffset for CreatedAt/UpdatedAt (includes timezone offset)
- Better for multi-tenant global apps (know exact UTC moment)
- .NET 10 standard for timestamp columns
- Avoids timezone confusion across regions
5. **ITenantEntity Interface Pattern**
- Explicit interface property (not EF shadow property) allows:
- Easy data seeding in tests
- LINQ queries without special knowledge
- Clear contract in domain code
- Marker interface only (no methods) — true DDD boundary
6. **Entity Lifecycle Simplicity**
- No domain events (YAGNI for MVP)
- No navigation properties (deferred to EF configuration in Task 7)
- No validation attributes — EF Fluent API handles in Task 7
- State machine is only behavior (business rule enforcement)
7. **Test Structure for TDD**
- Helper factory method `CreateWorkItem()` reduces repetition
- AAA pattern (Arrange-Act-Assert) clear in test names
- Arrange: Create entity with minimal valid state
- Act: Call transition method
- Assert: Verify state changed or exception thrown
- xUnit [Fact] attributes sufficient (no [Theory] needed for now)
8. **Project Structure Observations**
- Projects in `backend/` root, not `backend/src/` (deviation from typical convention but works)
- Subdirectories: Entities/, Enums/, Interfaces/ (clean separation)
- Test mirrors source structure: Domain tests in dedicated folder
- Class1.cs stub removed before implementation
### Files Created
- `WorkClub.Domain/Enums/SportType.cs` — 5 values
- `WorkClub.Domain/Enums/ClubRole.cs` — 4 values
- `WorkClub.Domain/Enums/WorkItemStatus.cs` — 5 values
- `WorkClub.Domain/Interfaces/ITenantEntity.cs` — Marker interface
- `WorkClub.Domain/Entities/Club.cs` — Basic aggregate root
- `WorkClub.Domain/Entities/Member.cs` — User representation
- `WorkClub.Domain/Entities/WorkItem.cs` — Task with state machine
- `WorkClub.Domain/Entities/Shift.cs` — Volunteer shift
- `WorkClub.Domain/Entities/ShiftSignup.cs` — Shift registration
- `WorkClub.Tests.Unit/Domain/WorkItemStatusTests.cs` — 12 tests, all passing
### Build & Test Results
- **Tests**: 12/12 passed (100%)
- 5 valid transition tests ✓
- 5 invalid transition tests ✓
- 2 CanTransitionTo() method tests ✓
- **Build**: Release configuration successful
- **Warnings**: Only Finbuckle version resolution (expected, no errors)
### Next Steps (Tasks 5-7)
- Task 5: Next.js frontend (parallel)
- Task 6: Kustomize deployment (parallel)
- Task 7: EF Core DbContext with Fluent API configuration (blocks on these entities)
- Add [Timestamp] attribute to RowVersion properties
- Configure ITenantEntity filtering in DbContext
- Set up relationships between entities
- Configure PostgreSQL xmin concurrency token