# CC Soccer - Entity Specification Template

**Purpose:** Complete specification of all entities with full attribute details, validation rules, relationships, and business logic.

**Instructions:** Fill in each section as completely as possible. This will serve as the authoritative reference for development.

**Date Started:** [Date]  
**Last Updated:** [Date]  
**Completed By:** [Your name]

---

## Entity 1: League

**Description:** Different league types (Coed, Men's, Tournament Series) with default settings.

**Database Table:** `league`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| name | String (255) | Yes | | League name | "Coed League", "Men's League" |
| type | List | Yes | | League type | coed, mens, coed35, womens |
| team_size_min | Integer | | | Minimum players per team | 6 |
| team_size_max | Integer | | | Maximum players per team | 10 |
| default_price | Decimal | | | Default registration price | 120.00 |
| day_of_week | List | | | Default game day | Tuesday, Thursday |
| time_slots | Multi-value List | | | Available time slots | 6:00pm, 7:00pm, 8:00pm |
| fields_available | Multi-value List | | | Available fields | Field 1, Field 2, Field 3 |
| game_duration | Integer | | 60 | Game length in minutes | 60 |
| description | Text (long) | No | | League description | |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |
| | | | | | |

---

### Relationships

**Has Many:**
- Seasons (one league → many seasons)

**Belongs To:**
- None (top-level entity)

**Note:** Tournaments do NOT belong to leagues - they are standalone entities.

---

### Validation Rules

1. **Name:** Must be unique
2. **Type:** Must be one of: coed, mens, tournament_series
3. **Team sizes:** team_size_min < team_size_max
4. **Price:** Must be >= 0
5. **ADD MORE RULES:**

---

### Business Logic

**When creating a League:**
- Set default values for time_slots and fields_available based on type

**When editing a League:**
- Changes affect NEW seasons only (existing seasons keep their settings)

**When deleting a League:**
- Cannot delete if has active seasons
- Archive instead?

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View:** All roles
- **Create:** Admin only
- **Edit:** Admin only
- **Delete:** Admin only

---

### UI/Display Notes

**Admin list view shows:**
- Name
- Type
- Active seasons count
- Last used

**Form sections:**
- Basic Info (name, type, description)
- Team Settings (sizes)
- Game Settings (day, time slots, fields, duration)
- Pricing (default price)

---

### Questions/TODOs

- [ ] Do we need "active/inactive" status?
- [ ] Should we track "seasons per year" (Spring, Summer, Fall)?
- [ ] Any league-specific rules (e.g., Mens requires age 35+)?

---

---

## Entity 2: Season

**Description:** Instance of a league at a point in time (regular weekly play ONLY). Tournaments are a separate entity.

**Database Table:** `season`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| league | Entity Reference (League) | Yes | | Parent league | League ID: 1 |
| name | String (255) | Yes | | Season name | "Fall 2025 Coed" |
| start_date | Date | Yes | | First game date | 2025-09-02 |
| end_date | Date | Yes | | Last game date | 2025-11-18 |
| registration_open | DateTime | Yes | | Registration opens | 2025-08-01 09:00:00 |
| registration_close | DateTime | Yes | | Registration closes | 2025-08-25 23:59:59 |
| max_players | Integer | Yes | | Maximum registrations | 144 (18 teams × 8) |
| reserved_spots | Integer | No | 0 | Spots reserved for waitlist | 0, 1, 2 |
| price | Decimal (Commerce Price) | | | Registration fee (can override league) | 120.00 |
| skip_days | Multi-value Date | No | | Holidays/breaks | 2025-11-26 (Thanksgiving) |
| schedule_generated | Boolean | No | FALSE | Has schedule been created? | FALSE, TRUE |
| schedule_parameters | Text (JSON) | No | | Last schedule params used | {"fields": 3, "time_slots": 3} |
| team_size_min | Integer | No | | Override league default | 6 |
| team_size_max | Integer | No | | Override league default | 10 |
| status | List | Yes | planned | Current status | planned, registration_open, in_progress, completed, cancelled |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |
| | | | | | |

---

### Relationships

**Belongs To:**
- League (many seasons → one league)

**Has Many:**
- Registrations (one season → many registrations)
- Teams (one season → many teams)
- Games (one season → many games)

---

### Calculated/Helper Methods

**isFull()** - Returns TRUE if no spots available
```
Logic: (count paid registrations) >= (max_players - reserved_spots)
```

**getSpotsRemaining()** - Returns number of open spots
```
Logic: max_players - count(paid registrations) - reserved_spots
```

**isRegistrationOpen()** - Returns TRUE if accepting registrations
```
Logic: 
- registration_open <= NOW <= registration_close
- AND has spots remaining (or user has override)
```

**getGamesPlayed()** - Count completed games
```
Logic: Count games where game_date < TODAY AND status = completed
```

**ADD MORE METHODS:**

---

### Validation Rules

1. **Dates:** start_date < end_date
2. **Registration:** registration_open < registration_close < start_date
3. **Capacity:** max_players > 0
4. **Reserved spots:** reserved_spots <= max_players
5. **Type:** Must be "season" or "tournament"
6. **ADD MORE RULES:**

---

### Business Logic

**When creating a Season:**
- Inherit default settings from League
- Generate unique name if blank (e.g., "Fall 2025 [League Name]")
- Create associated Commerce Product automatically?

**When registration opens:**
- Status changes to "registration_open"
- Notification sent to previous season players?

**When season fills:**
- Subsequent registrations go to waitlist
- Notification sent to board/admin

**When season starts:**
- Status changes to "in_progress"
- Registration closes (if not already)

**When season ends:**
- Status changes to "completed"
- Trigger credit expiration (1 year from end?)

**When someone cancels registration:**
- If waitlist exists: increment reserved_spots, notify next person
- If no waitlist: decrement registered count (spot opens)

**ADD MORE LOGIC:**

---

### Season vs Tournament Differences

| Aspect | Season | Tournament |
|--------|--------|------------|
| Duration | 10-12 weeks | 1 day |
| Registration Flow | Individual → Groups | Captain → Team OR Join Team |
| Team Formation | Admin generates after reg closes | Players form teams during reg |
| Deposits | No | Yes (for captains) |
| Jersey | Auto-add if first time | No jersey |
| Credits | Yes | No |
| Waitlist | Yes | No |
| **ADD MORE DIFFERENCES:** | | |

---

### Access Permissions

**Who can:**
- **View:** All roles (public can see available seasons)
- **Create:** Admin only
- **Edit:** Admin only
- **Delete:** Admin only (maybe never? Archive instead?)

---

### UI/Display Notes

**Public registration list shows:**
- Season name
- League type (Coed/Men's/Tournament)
- Dates
- Price
- Spots remaining (or "Full - Join Waitlist")
- Registration deadline

**Admin list shows:**
- Name
- Type
- Status
- Registration dates
- Registered / Max
- Schedule generated?

**Admin detail page sections:**
- Basic Info
- Registration Settings
- Schedule Settings (skip days)
- Player List
- Waitlist
- Actions (Generate Roster, Generate Schedule)

---

### Questions/TODOs

- [ ] Can a season be re-opened after closing?
- [ ] What happens if someone registers after registration_close? (overrides only?)
- [ ] Should we auto-close registration when full?
- [ ] How to handle season extensions (e.g., playoffs)?
- [ ] Track weather cancellations separately from skip_days?

---

---

## Entity 3: Tournament

**Description:** Standalone tournament events. NOT tied to leagues. Open to all players (season regulars and non-regulars).

**Database Table:** `tournament`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| name | String (255) | Yes | | Tournament name | "Summer Kickoff 2025" |
| start_date | Date | Yes | | Tournament date | 2025-07-12 |
| end_date | Date | Yes | | Usually same as start | 2025-07-12 |
| registration_open | DateTime | Yes | | Registration opens | 2025-06-01 09:00:00 |
| registration_close | DateTime | Yes | | Registration closes | 2025-07-10 23:59:59 |
| max_teams | Integer | Yes | | Maximum teams allowed | 8, 16, 32 |
| deposit_amount | Decimal | Yes | 50.00 | Captain deposit | 50.00 |
| registration_price | Decimal | Yes | 80.00 | Per-player fee | 80.00 |
| age_requirement | Integer | Yes | 18 | Minimum age | 18, 21 |
| gender_requirement | List | Yes | coed | Gender rules | coed, mens, open |
| location | String (255) | No | | Venue/park name | "Laguna Lake Park" |
| fields_available | Multi-value List | No | | Fields for tournament | Field A, Field B |
| format | List | Yes | round_robin | Tournament format | bracket, round_robin, pool_play |
| schedule_generated | Boolean | No | FALSE | Has schedule been created? | FALSE, TRUE |
| schedule_parameters | Text (JSON) | No | | Last schedule params | {"rounds": 3} |
| status | List | Yes | planned | Current status | planned, registration_open, in_progress, completed, cancelled |
| description | Text (long) | No | | Tournament details | |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |

---

### Relationships

**Has Many:**
- Registrations (one tournament → many registrations)
- Teams (one tournament → many teams)
- Games (one tournament → many games)

**Belongs To:**
- None (standalone entity, NOT tied to leagues)

**Key Difference from Seasons:**
- Tournaments track max_teams (not max_players)
- No connection to League entity
- Open to anyone (not just league players)

---

### Validation Rules

1. **Dates:** start_date <= end_date (usually same day)
2. **Registration:** registration_open < registration_close <= start_date
3. **Capacity:** max_teams > 0
4. **Deposit:** deposit_amount >= 0
5. **Price:** registration_price > 0
6. **Age:** age_requirement >= 18 (typically)
7. **ADD MORE RULES:**

---

### Business Logic

**When creating a Tournament:**
- Set default values (deposit $50, registration $80)
- No league connection needed
- Generate unique name

**When registration opens:**
- Status changes to "registration_open"
- Captains can register and form teams
- Non-captains can join existing teams

**When tournament fills (max_teams reached):**
- Close registration
- No waitlist system (unlike seasons)

**When tournament starts:**
- Status changes to "in_progress"
- Teams are locked (no roster changes)

**When tournament ends:**
- Status changes to "completed"
- No credits issued (unlike seasons)

**Captain Registration Flow:**
1. Captain pays deposit + registration fee
2. Creates team with custom name
3. Can invite players to team
4. Manages team roster

**Player Registration Flow:**
1. Player selects existing team from dropdown
2. Pays registration fee (no deposit)
3. Added to team roster
4. Captain can remove if mistake

**ADD MORE LOGIC:**

---

### Tournament vs Season Differences

| Aspect | Tournament | Season |
|--------|------------|--------|
| Belongs to League | NO | YES |
| Duration | 1 day | 10-12 weeks |
| Team Formation | Captain-led | Admin-generated |
| Capacity | max_teams | max_players |
| Deposit | Yes (captains) | No |
| Jersey | No | Yes (first time) |
| Waitlist | No | Yes |
| Credits | No | Yes |
| Skip Days | No | Yes |
| Reserved Spots | No | Yes |
| **ADD MORE DIFFERENCES:** | | |

---

### Access Permissions

**Who can:**
- **View:** All roles (public can see available tournaments)
- **Create:** Admin only
- **Edit:** Admin only
- **Delete:** Admin only
- **Manage Teams:** Captain (their own team), Admin (all teams)

---

### UI/Display Notes

**Public registration list shows:**
- Tournament name
- Date
- Location
- Registration price
- Deposit amount (for captains)
- Teams registered / Max teams
- Registration deadline

**Admin list shows:**
- Name
- Date
- Status
- Teams / Max
- Schedule generated?
- Captain list

**Captain team management shows:**
- Team name (can edit)
- Current roster
- Invite players (enter emails)
- Remove players

---

### Questions/TODOs

- [ ] Can a tournament have multiple days? (rare)
- [ ] Bracket generation algorithm?
- [ ] Prize/awards tracking?
- [ ] Free agent handling? (players without teams)
- [ ] Refund policy if tournament cancelled?
- [ ] Team size limits (min/max players per team)?

---

---

## Entity 4: Team

**Description:** Teams for seasons (admin-generated, balanced) OR tournaments (captain-formed, player-chosen). Each team belongs to EITHER a season OR a tournament, never both.

**Database Table:** `team`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| season | Entity Reference (Season) | Conditional | | Parent season (XOR with tournament) | Season ID: 5 |
| tournament | Entity Reference (Tournament) | Conditional | | Parent tournament (XOR with season) | Tournament ID: 2 |
| name | String (255) | Yes | | Team name | "Team 1", "Blue Thunder" |
| players | Multi-value Entity Reference (User) | | | Team members | User IDs: [15, 22, 38...] |
| captain | Entity Reference (User) | Conditional | | Team captain (REQUIRED for tournaments) | User ID: 15 |
| color | List | No | | Jersey color | Red, Blue, Green, Yellow, Orange, Purple |
| average_skill | Decimal | No | | Calculated avg skill | 6.5 |
| average_age | Decimal | No | | Calculated avg age | 32.4 |
| goalie_count | Integer | No | 0 | Number of goalies | 0, 1, 2 |
| women_count | Integer | No | 0 | Number of women | 2 |
| created_date | DateTime | Yes | NOW | When team was formed | 2025-08-26 10:30:00 |
| notes | Text (long) | No | | Admin notes | "Strong defensive team" |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |
| | | | | | |

---

### Relationships

**Belongs To:**
- Season OR Tournament (must have one, not both, not neither)
- Captain (required for tournament teams, null for season teams)

**Has Many:**
- Players (one team → many users)
- Games (as team_1 or team_2)

**Validation:**
- Must reference season XOR tournament (exactly one)
- If tournament: captain is REQUIRED
- If season: captain should be NULL

---

### Calculated/Helper Methods

**calculateAverageSkill()** - Avg of all players' league_score
```
Logic: SUM(player.field_skill_level) / COUNT(players)
```

**calculateAverageAge()** - Avg of all players' ages
```
Logic: SUM(age from DOB) / COUNT(players)
```

**countGoalies()** - How many players prefer goalie
```
Logic: COUNT(players WHERE field_prefers_goalie = TRUE)
```

**countWomen()** - How many women on team
```
Logic: COUNT(players WHERE field_gender = 'Female')
```

**ADD MORE METHODS:**

---

### Validation Rules

1. **Name:** Must be unique within season
2. **Season:** Must reference existing season
3. **Players:** Count must be between season.team_size_min and team_size_max
4. **Captain:** Must be one of the players
5. **ADD MORE RULES:**

---

### Business Logic

**For Regular Seasons:**
- Teams generated by admin using TeamBalancerService
- Names default to "Team 1", "Team 2", etc.
- Board member (Slofriendly) can rename teams
- Cannot be edited by players
- Locked after roster builder approves

**For Tournaments:**
- Captain creates team during registration (pays deposit)
- Captain chooses team name
- Captain can invite/remove players
- Players can join existing teams
- Can be edited until tournament starts

**Balancing Priorities (Regular Seasons):**
1. Groups stay together (hard constraint)
2. Skill distribution (primary factor)
3. Gender distribution (at least 1 woman per team if possible)
4. Goalie distribution (ideally 1-2 per team)
5. Age variance (secondary factor)

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View:** All authenticated users (can see their own team, all teams in their season)
- **Create:** Admin (seasons) OR Captain (tournaments)
- **Edit:** Admin OR Captain (only their own tournament team)
- **Delete:** Admin only

---

### UI/Display Notes

**Public team page shows:**
- Team name
- Season
- Player list (names, pictures?)
- Schedule (just their games)
- Captain contact (tournaments)

**Admin roster builder shows:**
- All teams side-by-side
- Each team's stats (avg skill, avg age, goalies, women)
- Suggestions for improving balance
- Drag-drop to move players

---

### Questions/TODOs

- [ ] Do we show player phone numbers on team page? (privacy concern)
- [ ] Can teams be archived after season ends?
- [ ] Track team performance/stats (wins/losses)?
- [ ] Allow players to message their teammates?
- [ ] Team colors - assigned randomly or chosen?

---

---

## Entity 4: Game

**Description:** Individual scheduled games for seasons OR tournaments. Each game belongs to EITHER a season OR a tournament.

**Database Table:** `game`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| season | Entity Reference (Season) | Conditional | | Parent season (XOR with tournament) | Season ID: 5 |
| tournament | Entity Reference (Tournament) | Conditional | | Parent tournament (XOR with season) | Tournament ID: 2 |
| game_date | Date | Yes | | Date of game | 2025-09-02 |
| game_time | Time | Yes | | Start time | 18:00:00 (6:00pm) |
| team_1 | Entity Reference (Team) | Yes | | First team | Team ID: 12 |
| team_2 | Entity Reference (Team) | Yes | | Second team | Team ID: 15 |
| field | List | Yes | | Playing field | Field 1, Field 2, Field 3 |
| time_slot | Integer | Yes | | Time slot number | 1, 2, 3 |
| week_number | Integer | Yes | | Week of season | 1, 2, 3...12 |
| game_duration | Integer | No | 60 | Duration in minutes | 60 |
| status | List | Yes | scheduled | Current status | scheduled, cancelled, completed, postponed |
| cancellation_reason | Text | No | | Why cancelled/postponed | "Rain", "Field closed" |
| referee | Entity Reference (User) | No | | Assigned referee (future) | User ID: 99 |
| notes | Text (long) | No | | Admin notes | |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |
| | | | | | |

---

### Relationships

**Belongs To:**
- Season OR Tournament (must have one, not both, not neither)
- Team 1 (many games → one team)
- Team 2 (many games → one team)
- Referee (optional - many games → one user)

**Validation:**
- Must reference season XOR tournament (exactly one)
- team_1 and team_2 must belong to same season/tournament

---

### Validation Rules

1. **Parent:** Must have season XOR tournament (exactly one)
2. **Teams:** team_1 != team_2 (can't play self)
3. **Teams:** Both teams must belong to same season/tournament
4. **Date:** game_date must be between parent's start_date and end_date
5. **Date:** (seasons only) game_date should NOT be in season.skip_days
6. **Time slot:** Must be valid for the league (seasons) or tournament
7. **Field:** Must be valid for the league (seasons) or tournament
8. **Status:** Valid transitions (scheduled → cancelled/completed, not completed → scheduled)
7. **ADD MORE RULES:**

---

### Business Logic

**When game is created:**
- Default status = "scheduled"
- Default duration from league settings

**At 3pm on game day:**
- Check for cancellations
- If status = "cancelled":
  - Send notifications to both teams
  - Issue credits to players (reg_fee / total_games * 0.75)
  - Post to social media (future)

**Day after game:**
- Reset status? (for next week's check)
- Or keep as completed/cancelled for history

**When manually cancelled:**
- Update status
- Set cancellation_reason
- Trigger notifications and credits

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View:** All authenticated users
- **Create:** Admin only (via schedule generator)
- **Edit:** Admin, Board Member
- **Delete:** Admin only
- **Update Status:** Admin, Board Member

---

### UI/Display Notes

**Public schedule shows:**
- Date, Time
- Field
- Team 1 vs Team 2
- Status (if cancelled)
- Week number

**Display options:**
- Calendar view (FullCalendar)
- List view (sortable table)
- By team (filter)
- Export: iCal, PDF

**Admin schedule builder shows:**
- Full season grid
- Color-coded by time slot
- Counts per team per time slot
- Ability to swap games

---

### Questions/TODOs

- [ ] Track scores? (probably not for rec league)
- [ ] Allow captains to report game status?
- [ ] Game check-in system? (who showed up)
- [ ] Automated reminders (24 hours before game)?
- [ ] Handle forfeits?

---

---

## Entity 5: Credits

**Description:** Track credit history for audit trail. Each credit transaction is a separate record. **SEASONS ONLY - Tournaments do NOT use credits.**

**Database Table:** `credits`

**Entity Type:** Custom Entity

---

### Fields/Attributes

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| id | Integer (auto) | Yes | Auto | Primary key | 1, 2, 3 |
| uuid | UUID | Yes | Auto | Universal ID | |
| user | Entity Reference (User) | Yes | | Who owns credit | User ID: 25 |
| amount | Decimal (10,2) | Yes | | Credit amount | 45.50 |
| type | List | Yes | | Transaction type | earned, used, expired, adjusted |
| source | List | Yes | | Where it came from | registration_cancelled, refund, admin_adjustment, rain_out |
| source_registration | Entity Reference (Registration) | No | | Related registration (MUST be season registration) | Registration ID: 123 |
| source_order | Entity Reference (Order) | No | | Related order | Order ID: 456 |
| created | Timestamp | Yes | NOW | When issued | 2025-09-15 14:30:00 |
| expires | Timestamp | Yes | NOW+1year | When expires | 2026-09-15 23:59:59 |
| used_date | Timestamp | No | | When used | 2025-10-01 10:00:00 |
| status | List | Yes | active | Current status | active, used, expired |
| reason | Text (long) | Yes | | Explanation | "Cancelled after 2 of 12 games" |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |
| | | | | | |

---

### Relationships

**Belongs To:**
- User (many credits → one user)
- Registration (optional - many credits → one registration)
- Order (optional - many credits → one order)

**User Also Has:**
- field_credits_balance (calculated field for quick lookup)

---

### Calculated/Helper Methods

**isExpired()** - Check if expired
```
Logic: (expires < NOW) AND (status = 'active')
```

**isActive()** - Check if usable
```
Logic: (status = 'active') AND (expires > NOW)
```

**ADD MORE METHODS:**

---

### Validation Rules

1. **Amount:** Must be > 0
2. **Expires:** Must be > created
3. **Type:** Must match source (e.g., type=used requires used_date)
4. **Status transitions:** active → used/expired only
5. **ADD MORE RULES:**

---

### Business Logic

**When credit is earned (cancellation):**
```
Formula: (total_paid / total_games) * games_remaining * 0.75

Example:
- Paid: $145 (reg + jersey)
- Total games: 12
- Games played: 2
- Remaining: 10
- Credit: ($145 / 12) * 10 * 0.75 = $90.63
```

**When credit is earned (rain out):**
```
Formula: (registration_fee / total_games) * 0.75

Example:
- Reg fee: $120
- Total games: 12
- Credit per game: ($120 / 12) * 0.75 = $7.50
```

**When credit is used:**
- Applied to order at checkout
- Reduces total_due
- Mark credit as used (status = 'used', used_date = NOW)
- Decrement user.field_credits_balance

**When credit expires:**
- Cron job runs nightly
- Find credits where expires < NOW AND status = 'active'
- Update status = 'expired'
- Decrement user.field_credits_balance
- Optional: Send notification

**Refund vs Credit:**
- Default: Issue credit
- Before season starts: Option for refund
- Special cases (moving, emergency): Admin can issue refund
- Refunds processed through Commerce, not via Credits entity

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View Own:** All authenticated users (their own credits)
- **View All:** Admin, Board Member
- **Create:** System only (via CreditManagerService)
- **Edit:** Admin only (for adjustments)
- **Delete:** Never (audit trail)

---

### UI/Display Notes

**Player's credit page shows:**
- Current balance (highlighted)
- Credit history table:
  - Date earned/used
  - Amount
  - Source/Reason
  - Expires
  - Status

**Admin credit management shows:**
- Search by player
- All credit transactions
- Ability to manually adjust (create adjustment credit)
- Export for reporting

---

### Questions/TODOs

- [ ] Should expired credits be hidden from player view?
- [ ] Email notification when credits about to expire (30 days)?
- [ ] Can credits be transferred between players?
- [ ] Maximum credit balance limit?
- [ ] Different expiration periods for different sources?

---

---

## Entity 6: Registration (Extended from Contrib Module)

**Description:** Player registration for a season OR tournament. Extends the contrib Registration module with custom fields. Different fields apply based on whether it's a season or tournament registration.

**Database Table:** `registration` (from contrib) + custom field tables

**Entity Type:** Content Entity (from Registration module, we extend it)

**Base Fields from Contrib:**
- id, uuid, created, changed
- status (we customize the values)
- Capacity management
- Waitlist support

---

### Custom Fields We Add

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| season | Entity Reference (Season) | Conditional | | Season (XOR with tournament) | Season ID: 5 |
| tournament | Entity Reference (Tournament) | Conditional | | Tournament (XOR with season) | Tournament ID: 2 |
| player | Entity Reference (User) | Yes | | The player | User ID: 25 |
| jersey_size | List | Conditional | | Jersey size (SEASONS ONLY, if first reg) | S, M, L, XL, XXL |
| skill_level | Integer (1-10) | No | | Admin-adjusted skill | 7 |
| self_score | Integer (1-10) | Conditional | | Player's self-assessment | 6 |
| prefers_goalie | Boolean | Conditional | FALSE | Wants to play goalie? | TRUE, FALSE |
| group_id | String (255) | No | | Group identifier (SEASONS ONLY) | "smith-fall-2025" |
| invited_by | Entity Reference (User) | No | | Who invited them | User ID: 20 |
| invitation_status | List | No | none | Invitation state | none, pending, accepted, declined |
| team | Entity Reference (Team) | No | | Assigned team (after roster gen or captain selection) | Team ID: 12 |
| commerce_order | Entity Reference (Order) | Yes | | Payment order | Order ID: 789 |
| commerce_line_item | Entity Reference (Line Item) | Yes | | Line item | Line Item ID: 890 |
| credits_used | Decimal | No | 0.00 | Credits applied (SEASONS ONLY) | 30.00 |
| has_override | Boolean | No | FALSE | Has priority registration (SEASONS ONLY) | TRUE, FALSE |
| override_expires | DateTime | No | | Override expiration (SEASONS ONLY) | 2025-08-15 23:59:59 |
| override_notified | DateTime | No | | When override notification sent (SEASONS ONLY) | 2025-08-13 10:00:00 |
| registration_type | List | Yes | | Season or tournament | season_registration, tournament_registration |
| tournament_deposit_paid | Boolean | No | FALSE | Captain paid deposit? (TOURNAMENTS ONLY) | TRUE, FALSE |
| is_captain | Boolean | No | FALSE | Is team captain? (TOURNAMENTS ONLY) | TRUE, FALSE |
| waiver_signed | Boolean | Yes | FALSE | Waiver accepted | TRUE, FALSE |
| waiver_date | DateTime | No | | When waiver signed | 2025-08-20 10:30:00 |
| waiver_image | File/Image | No | | Uploaded signed waiver | |
| status | List | Yes | pending | Registration status | pending, paid, active, cancelled, waitlist, expired |
| cancellation_date | DateTime | No | | When cancelled | 2025-09-10 14:00:00 |
| cancellation_reason | Text | No | | Why cancelled | "Moving out of state" |
| notes | Text (long) | No | | Admin notes | |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |

---

### Relationships

**Belongs To:**
- User/Player (many registrations → one user)
- Season OR Tournament (must have one, not both, not neither)
- Team (optional - many registrations → one team)
- Order (many registrations → one order)

**References:**
- Invited By (user who invited them)
- Group (logical grouping via group_id - seasons only)

**Validation:**
- Must reference season XOR tournament (exactly one)
- If season: can use group_id, override, credits, waitlist
- If tournament: can use deposit/captain logic, no override/waitlist/credits

---

### Status Values & Meanings

| Status | Meaning | Can Register? | Payment? |
|--------|---------|---------------|----------|
| pending | Checkout started, not paid | N/A | Pending |
| paid | Payment received | Yes | Complete |
| active | Playing in season | Yes | Complete |
| cancelled | Registration cancelled | No | Refunded/Credited |
| waitlist | On waitlist | No (unless override) | No payment yet |
| expired | Override expired unused | No | No |

**ADD MORE STATUS VALUES:**

---

### Validation Rules

1. **Player + Season:** Must be unique (no duplicate registrations)
2. **Age:** Player must meet league age requirements (18+ coed, 35+ mens) UNLESS permanent_override
3. **Gender:** Player must meet league gender requirements (mens only) UNLESS permanent_override
4. **Capacity:** Season must have spots available OR has_override = TRUE
5. **Waiver:** Must be signed before payment
6. **Jersey:** Required if player.field_jersey_size is empty
7. **Override:** If has_override, must have override_expires
8. **Group:** If group_id set, must have invited_by
9. **Tournament:** If tournament, must select team OR pay deposit
10. **ADD MORE RULES:**

---

### Business Logic

**Season Registration Flow:**
1. Add to cart
2. Checkout Step 1: Player info (if first time)
3. Checkout Step 2: Group selection
4. Checkout Step 3: Credits application
5. Checkout Step 4: Waiver
6. Checkout Step 5: Payment
7. On checkout complete: Create Registration (status=pending)
8. On payment complete: Update status=paid
9. Send confirmation
10. Redirect to group invitation page

**Tournament Registration Flow:**
1. Add to cart
2. Checkout Step 1: Player info waiver
3. Checkout Step 2: Team selection
   - Pay deposit (become captain) + enter team name
   - OR Join existing team (dropdown)
   - OR Free agent
4. Checkout Step 3: Credits (if applicable)
5. Checkout Step 4: Payment
6. On payment: Create Registration
7. If captain: notify slofriendly
8. If joining: add to team's group

**Cancellation Flow:**
1. Admin cancels registration
2. Check if season started (games played?)
3. Calculate credit amount:
   - Before first game: Full amount (or refund if requested)
   - After games: (total_paid / total_games) * games_remaining * 0.75
4. Issue credit (or process refund)
5. Update status = cancelled
6. Check for waitlist:
   - If exists: increment reserved_spots, notify next person
   - If not: spot opens for anyone
7. Log cancellation

**Waitlist Flow:**
1. Season fills up
2. New registration → status = waitlist
3. When spot opens (cancellation):
   - Grant override to next waitlist person
   - has_override = TRUE
   - override_expires = NOW + 2 days
   - Send notification
4. If they register within window:
   - Process normal registration
   - Update status = paid
5. If expires:
   - has_override = FALSE
   - Pass to next waitlist person

**Override Flow:**
1. Admin grants override (or waitlist automation)
2. has_override = TRUE, override_expires = DATE
3. Send notification
4. User registers within timeframe
5. Override consumed (has_override = FALSE)
6. Registration proceeds normally

**Group Invitation Flow:**
1. User completes registration
2. Creates group (optional)
3. Enters emails to invite
4. System creates pending registrations for invitees:
   - group_id = same
   - invited_by = inviter
   - invitation_status = pending
5. Invitee receives email with link
6. Invitee registers
7. invitation_status = accepted
8. Group members visible to each other

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View Own:** All authenticated users
- **View All:** Admin, Board Member
- **Create:** System (via checkout process)
- **Edit Own:** Limited (can update group invitations only)
- **Edit All:** Admin (can update any field)
- **Cancel:** Admin only

---

### UI/Display Notes

**Player's registration page shows:**
- Current registrations (active seasons)
- Past registrations (history)
- For each:
  - Season name
  - Team assignment (if available)
  - Payment details
  - Group members (if in group)
  - Waiver link

**Admin registration list shows:**
- Filterable table:
  - Player name
  - Season
  - Status
  - Jersey size
  - Team (if assigned)
  - Payment status
  - Registration date
- Bulk actions: Export, Cancel
- Search by player/season

**Admin registration detail shows:**
- All fields
- Related order details
- Credit usage
- Group members
- Actions: Cancel, Issue Override, Move to Team

---

### Questions/TODOs

- [ ] Can players update their jersey size after registration?
- [ ] How long keep cancelled registrations? Archive?
- [ ] Allow players to cancel their own registration (with approval)?
- [ ] Track partial payments?
- [ ] Support for payment plans?

---

---

## Entity 7: User (Extended from Drupal Core)

**Description:** Player accounts. We extend Drupal's User entity with custom fields for player attributes.

**Database Table:** `users` (core) + field tables

**Entity Type:** User Entity (Drupal core)

**Core Fields:**
- uid, uuid, name, mail, pass
- roles, status, created, access, login

---

### Custom Fields We Add

| Field Name | Data Type | Required? | Default Value | Description | Example Values |
|------------|-----------|-----------|---------------|-------------|----------------|
| field_first_name | String (255) | Yes | | First name | "John" |
| field_last_name | String (255) | Yes | | Last name | "Smith" |
| field_phone | Telephone | Yes | | Phone number | "(805) 555-1234" |
| field_dob | Date | Yes | | Date of birth | 1988-05-15 |
| field_gender | List | Yes | | Gender | Male, Female, Other, Prefer not to say |
| field_zip_code | String (10) | No | | Zip code | "93401" |
| field_jersey_size | List | No | | Jersey size (set on first reg) | S, M, L, XL, XXL |
| field_skill_level | Integer (1-10) | No | | Admin-set skill (authoritative) | 7 |
| field_self_score | Integer (1-10) | No | | Player self-assessment | 6 |
| field_prefers_goalie | Boolean | No | FALSE | Wants to play goalie | TRUE, FALSE |
| field_credits_balance | Decimal (10,2) | No | 0.00 | Current credit balance | 45.50 |
| field_player_picture | Image | Yes | | Profile picture (required) | |
| field_notification_prefs | Multi-value List | No | email | How to notify | email, sms, both |
| field_emergency_name | String (255) | No | | Emergency contact name | "Jane Smith" |
| field_emergency_phone | Telephone | No | | Emergency contact phone | "(805) 555-5678" |
| field_discount_percent | Integer (0-100) | No | 0 | Discount % (board members) | 0, 10, 100 |
| field_permanent_override | Boolean | No | FALSE | Bypass age/gender checks | TRUE, FALSE |
| | | | | **ADD MORE FIELDS:** | |
| | | | | | |

---

### Roles & Permissions

| Role | Description | Key Permissions |
|------|-------------|-----------------|
| Anonymous | Not logged in | View schedules, view available registrations |
| Player | Default role | Register, manage profile, view own team/schedule |
| Captain | Tournament captains | + Invite to team, manage tournament roster |
| Board Member | Board/staff | + Jersey reports, notifications, game status |
| Slofriendly | Tournament director | + Team names, roster management, deposit reports |
| Admin | Site admin | Everything |
| Referee | Future | Game assignments, notifications |
| **ADD MORE ROLES:** | | |

---

### Validation Rules

1. **Email:** Must be unique
2. **Username:** Must be unique
3. **Age:** Must be 18+ to register for any league
4. **Phone:** Valid phone format
5. **Picture:** Required (enforced on registration)
6. **ADD MORE RULES:**

---

### Calculated/Derived Values

**Age:**
```
Logic: YEAR(NOW) - YEAR(field_dob)
```

**Can Register for Season:**
```
Logic:
- Check age vs league requirements
- Check gender vs league requirements
- UNLESS field_permanent_override = TRUE
```

**ADD MORE CALCULATIONS:**

---

### Business Logic

**On User Registration (creating account):**
- Require: email, first name, last name, DOB, gender, phone, picture
- Assign "Player" role by default
- Generate username from email (or let them choose)

**On First Season Registration:**
- Collect: jersey_size, self_score, prefers_goalie
- Save to user fields
- Auto-add jersey to cart

**On Subsequent Registrations:**
- Use existing jersey_size, self_score, prefers_goalie
- Don't auto-add jersey (already have one)

**Credit Balance:**
- Updated when credits issued/used/expired
- Cached for performance (don't recalculate every query)
- Recalculated on credit transactions

**Permanent Override:**
- Set by admin only
- Allows registration regardless of age/gender
- Does NOT bypass capacity limits
- Use cases: Board members in mens league, exceptional younger players

**Discount Percent:**
- Applied at checkout
- Board members typically 100% (pay nothing)
- Special promos might be 10-15%

**ADD MORE LOGIC:**

---

### Access Permissions

**Who can:**
- **View:** Own profile (always), others (limited fields)
- **Edit Own:** Name, phone, picture, notification prefs, emergency contact
- **Edit All:** Admin only
- **View Credits:** Own credits always, all credits (admin/board)

**Field Visibility:**
- **Public:** First name, last name, picture
- **Team Members:** Phone, email (if opted in)
- **Admin Only:** DOB, discount, permanent override, credits

---

### UI/Display Notes

**Player profile page shows:**
- Picture
- Name
- Email, Phone
- Current registrations
- Past registrations (history)
- Credit balance
- Team assignments

**Admin user management shows:**
- Searchable table
- Filter by role
- Bulk actions
- Fields: Name, Email, Roles, Credits, Last login

---

### Questions/TODOs

- [ ] Allow players to see other players' contact info? (Privacy concern)
- [ ] Two-factor authentication?
- [ ] Social login (Google, Facebook)?
- [ ] Player statistics/history page?
- [ ] Privacy settings (hide phone from team)?

---

---

## Entity 8: Order (Drupal Commerce - Used As-Is)

**Description:** Purchase orders from Drupal Commerce. We use this entity without modification.

**Database Table:** `commerce_order`

**Entity Type:** Commerce Entity (from contrib)

**We Use But Don't Modify**

---

### Key Fields (From Commerce)

| Field Name | Description |
|------------|-------------|
| order_id | Order ID |
| order_number | Human-readable order number |
| customer | User reference |
| order_items | Line items in order |
| total_price | Total amount |
| state | Order state (draft, completed, cancelled) |
| placed | When order placed |
| completed | When order completed |
| payment_gateway | Payment method used |
| payment_method | Specific payment details |

---

### How We Use It

**Registration Purchase:**
- User adds Season Registration to cart
- Optionally adds Jersey
- Optionally adds Tournament Deposit
- Creates Order
- On payment: Order state → completed
- On completed: Trigger registration creation

**Integration Points:**
- Hook: commerce_checkout_complete → Create Registration entity
- Hook: commerce_order_paid_in_full → Update Registration status
- View: Admin can see orders, issue refunds

---

---

## Entity 9: Product (Drupal Commerce - We Create Product Types)

**Description:** Products that can be purchased. We create product types but use Commerce's entity.

**Database Table:** `commerce_product`

**Entity Type:** Commerce Entity (from contrib)

---

### Product Types We Create

#### **1. Season Registration**

**Product Type:** season_registration

**Attributes:**
- Season (entity reference to Season entity)

**Variations:**
- One variation per season
- Price from season.price (or league default)
- SKU: "SEASON-{season_id}"

**Available When:**
- Season status = registration_open
- Spots remaining > 0 (or user has override)
- Registration date within window

---

#### **2. Tournament Registration**

**Product Type:** tournament_registration

**Attributes:**
- Tournament (entity reference to Season where type=tournament)

**Variations:**
- One variation per tournament
- Price from season.price
- SKU: "TOURN-{season_id}"

---

#### **3. Jersey**

**Product Type:** jersey

**Attributes:**
- Size (attribute value: S, M, L, XL, XXL)

**Variations:**
- One variation per size
- Price: $25 (configurable)
- SKU: "JERSEY-{size}"

**Availability:**
- Always available for purchase
- Auto-added to cart on first registration

---

#### **4. Tournament Deposit**

**Product Type:** tournament_deposit

**Attributes:**
- Tournament (entity reference)

**Variations:**
- One variation per tournament
- Price: $50 (configurable)
- SKU: "DEPOSIT-{season_id}"

**Logic:**
- Only available with tournament registration
- Marks buyer as captain
- Triggers team name field + slofriendly notification

---

### Questions/TODOs

- [ ] Early bird pricing? (different price before certain date)
- [ ] Discount codes/coupons?
- [ ] Merchandise (hats, bags, etc.)?
- [ ] Referee gear products?

---

---

## Entity 10: Message (Message Module - We Create Templates)

**Description:** Notification messages sent to users. We create message templates and let Message module handle storage/sending.

**Database Table:** `message`

**Entity Type:** Message Entity (from contrib)

---

### Message Templates We Create

#### **1. Registration Confirmation**

**Template ID:** registration_confirmation

**Sent When:** Payment completes for registration

**Recipients:** Registrant

**Variables:**
- [player_name]
- [season_name]
- [team_name] (if assigned)
- [amount_paid]
- [order_link]

**Subject:** Registration Confirmed - [season_name]

**Body:**
```
Hi [player_name],

Your registration for [season_name] has been confirmed!

Amount Paid: $[amount_paid]
Order #: [order_number]

[IF team assigned]
Team: [team_name]
[ENDIF]

[IF group created]
Manage your group: [group_link]
[ENDIF]

View schedule: [schedule_link]
View your registration: [registration_link]

Thanks for playing!
CC Soccer
```

---

#### **2. Waitlist Spot Available**

**Template ID:** waitlist_notification

**Sent When:** Spot opens, override granted to waitlist person

**Recipients:** Next person in waitlist

**Variables:**
- [player_name]
- [season_name]
- [expires_date]
- [registration_link]

**Subject:** Spot Available - [season_name]

**Body:**
```
Hi [player_name],

Good news! A spot has opened up in [season_name].

You have priority registration until [expires_date] (2 days).

Register now: [registration_link]

If you don't register by [expires_date], this spot will be offered to the next person on the waitlist.

CC Soccer
```

---

#### **3. Group Invitation**

**Sent When:** Group creator invites players

**Recipients:** Invitees

**ADD TEMPLATE DETAILS**

---

#### **4. Team Assignment**

**Sent When:** Roster finalized, teams published

**Recipients:** All players in season

**ADD TEMPLATE DETAILS**

---

#### **5. Schedule Published**

**Sent When:** Schedule generated and published

**Recipients:** All players in season

**ADD TEMPLATE DETAILS**

---

#### **6. Game Status Update**

**Sent When:** Game cancelled at 3pm

**Recipients:** Players on both teams

**ADD TEMPLATE DETAILS**

---

#### **7. Credit Issued**

**Sent When:** Credit issued for cancellation/rainout

**Recipients:** Player receiving credit

**ADD TEMPLATE DETAILS**

---

#### **8. Override Expiring Soon**

**Sent When:** 24 hours before override expires

**Recipients:** Player with expiring override

**ADD TEMPLATE DETAILS**

---

### Questions/TODOs

- [ ] Separate email vs SMS templates? (different content length)
- [ ] HTML email design/layout?
- [ ] Unsubscribe mechanism?
- [ ] Admin preview before sending?

---

---

## Cross-Entity Business Logic

### Capacity Management

**Scenario:** User tries to register for full season

**Logic:**
1. Check season.max_players
2. Count registrations where status IN ('paid', 'active')
3. Count season.reserved_spots
4. Calculate: spots_available = max_players - paid_count - reserved_spots
5. Check user.has_override
6. If spots_available > 0 OR has_override: Allow
7. Else: Add to waitlist

---

### Cancellation Credit Calculation

**Scenario:** Player cancels mid-season

**Logic:**
```php
$total_paid = $registration->commerce_order->total_price->number;
$total_games = count_games_for_season($season);
$games_played = count_games_before_date($season, $cancellation_date);
$games_remaining = $total_games - $games_played;

if ($games_played == 0) {
  // Before season started
  $credit_amount = $total_paid; // or offer refund
} else {
  // Mid-season
  $credit_amount = ($total_paid / $total_games) * $games_remaining * 0.75;
}

// Round to 2 decimals
$credit_amount = round($credit_amount, 2);
```

---

### Team Balancing Algorithm

**Scenario:** Admin generates teams for season

**Inputs:**
- All paid registrations for season
- Number of teams to create
- League settings (team_size_min, team_size_max)

**Constraints:**
1. Groups stay together (hard constraint)
2. Team size between min and max
3. Balance skill levels
4. At least 1 woman per team (if possible)
5. Distribute goalies evenly

**Priority Weighting:**
- Skill: 70%
- Age: 20%
- Gender: 10%
- Goalies: Evenly distribute

**Algorithm Steps:**
1. Get all paid registrations
2. Identify groups (registrations with same group_id)
3. Sort individuals by skill_level (descending)
4. Create N empty teams
5. Place groups first (distribute to teams, starting with strongest groups)
6. Snake draft individuals:
   - Round 1: Team 1 → Team N
   - Round 2: Team N → Team 1
   - Repeat
7. Balance goalies (swap if needed)
8. Balance women (swap if needed)
9. Calculate team stats (avg skill, avg age)
10. Check variance, suggest swaps if > threshold
11. Return teams for admin review

**Questions to Clarify:**
- [ ] Exact weight percentages for skill vs age?
- [ ] Acceptable variance in team scores? (within 5 points? 10?)
- [ ] What if not enough women for all teams?
- [ ] Goalie minimum per team? (1? 2?)

---

### Schedule Generation Algorithm

**Scenario:** Admin generates schedule for season

**Inputs:**
- All teams in season
- Season start/end dates
- Skip days
- Available fields (3)
- Available time slots (3)
- Day of week (Tuesday)

**Output:**
- Complete game schedule with dates, times, fields, matchups

**Constraints:**
1. Every team plays every other team once (round robin)
2. Roughly equal games at each time slot per team
3. Respect skip days
4. Fit within season dates

**Algorithm Steps:**
1. Generate round-robin matchups (standard algorithm)
2. Calculate total games needed
3. Calculate weeks needed (games / games_per_week)
4. Assign dates (start_date + week * 7, skip skip_days)
5. Assign fields (distribute evenly, alternate)
6. Assign initial time slots (rotate through 1, 2, 3)
7. POST-PROCESS: Balance time slots
   - Count each team's games per time slot
   - Target: equal distribution (e.g., 4 games each slot in 12-game season)
   - Swap game times to minimize variance
8. Create Game entities
9. Return schedule for preview

**Questions to Clarify:**
- [ ] Specific time slot balancing algorithm? (complex)
- [ ] Handle bye weeks (odd number of teams)?
- [ ] Prioritize certain fields/times for certain teams?
- [ ] Manual override capability important?

---

### Waitlist to Override Flow

**Scenario:** Player cancels, spot opens

**Logic:**
1. Detect: Registration status changed to 'cancelled'
2. Check: Is there a waitlist for this season?
   - Query: registrations where season = X AND status = 'waitlist'
   - Order by: created date (FIFO)
3. If waitlist exists:
   - Get next person (oldest waitlist entry)
   - Increment season.reserved_spots
   - Create override:
     - registration.has_override = TRUE
     - registration.override_expires = NOW + 2 days
     - registration.override_notified = NOW
   - Send notification (waitlist_notification template)
   - Log action
4. If no waitlist:
   - Decrement registered count
   - Spot opens for anyone (first come, first serve)
5. Cron job (hourly):
   - Check for expired overrides
   - If expires without registration:
     - Clear has_override
     - Pass to next waitlist person (repeat steps above)

---

### Age/Gender Validation

**Scenario:** User attempts to register for season

**Logic:**
```php
// Get league requirements
$league = $season->league;
$user_age = calculate_age($user->field_dob);
$user_gender = $user->field_gender;

// Check for permanent override
if ($user->field_permanent_override) {
  return TRUE; // Bypass all checks
}

// Age check
if ($league->type == 'coed' && $user_age < 18) {
  return ['allowed' => FALSE, 'reason' => 'Must be 18+ for Coed League'];
}
if ($league->type == 'mens' && $user_age < 35) {
  return ['allowed' => FALSE, 'reason' => 'Must be 35+ for Mens League'];
}

// Gender check
if ($league->type == 'mens' && $user_gender != 'Male') {
  return ['allowed' => FALSE, 'reason' => 'Mens League is for male players only'];
}

// All checks passed
return ['allowed' => TRUE];
```

---

### Group Management with Locking

**Scenario:** Groups before and after roster generation

**Before Roster Lock:**
- Players can create groups
- Players can invite others
- Players can join groups
- Group_id can be changed
- Flexible

**After Roster Lock (when admin runs roster builder):**
- Set season flag: roster_locked = TRUE
- Groups frozen (cannot change group_id)
- Team assignments visible
- Late registrations (with override):
  - Cannot join groups
  - Registered as individual
  - Admin manually assigns to team

---

---

## Integration Points

### Commerce → Registration

**Hook:** `hook_commerce_checkout_complete()`

**Triggers:** RegistrationService::processRegistration()

**Actions:**
1. Extract season from order line item
2. Create Registration entity
3. Link to order
4. Set status = 'pending'
5. Handle group logic (group_id, invited_by)
6. Apply credits if selected
7. Auto-add jersey if first registration

---

### Commerce → Registration (Payment)

**Hook:** `hook_commerce_order_paid_in_full()`

**Triggers:** RegistrationService::onPaymentComplete()

**Actions:**
1. Find registrations by order
2. Update status: pending → paid
3. Send confirmation notification
4. Check if season now full
5. If full: trigger waitlist notification

---

### Message → Symfony Mailer (Email)

**Integration:** Message Notify module routes to Symfony Mailer

**Configuration:**
- Message templates define content
- Message Notify sends via email channel
- Symfony Mailer handles SMTP/delivery

---

### Message → Clickatell (SMS)

**Integration:** Custom Message Notify plugin

**Implementation:**
- Build custom MessageNotifierInterface plugin
- Calls Clickatell REST API directly
- Format message for SMS (shorter, plain text)

---

### ECA → Automated Workflows

**Possible Uses:**
- Event: Order paid → Action: Update registration status
- Event: Override expires → Action: Notify next waitlist
- Event: Game date approaches → Action: Send reminder

**Or:** Implement in custom code (more control, easier debugging)

---

---

## Questions for Clarification

### High Priority

- [ ] **Team Balancing Weights:** Skill vs Age percentage? (70/30? 80/20?)
- [ ] **Team Balancing Threshold:** Acceptable variance in scores? (within 5? 10?)
- [ ] **Gender Distribution:** What if fewer women than teams?
- [ ] **Goalie Distribution:** Minimum per team? Ideal count?
- [ ] **Schedule Time Slot Balancing:** Specific algorithm? (you said "don't have solution")
- [ ] **Age Calculation:** As of registration date? Season start? Birthday during season?

### Medium Priority

- [ ] **First Registration:** First ever or first per season? (for jersey)
- [ ] **Waiver Storage:** Checkbox sufficient or need uploaded image?
- [ ] **Waiver Retention:** How long keep signed waivers?
- [ ] **Free Agents:** How to handle in tournaments?
- [ ] **Roster Lock Timing:** When exactly? After reg closes? Manual?

### Low Priority

- [ ] **Player Privacy:** Show phone/email to teammates?
- [ ] **Social Features:** Team messaging? Player profiles?
- [ ] **Statistics:** Track wins/losses? Individual stats?
- [ ] **Referee System:** Now or later?
- [ ] **Two-factor Auth:** Security requirement?

---

---

## Data Migration (from D7 to D11)

**Will Need to Migrate:**
- Users (with custom fields)
- Past registrations (history)
- Credits (active credits only? or all?)
- Teams (archive old seasons)
- Games (archive old seasons)

**Migration Strategy:**
- Use Drupal Migrate API
- Map D7 fields to D11 fields
- Test with subset first
- Validate data integrity
- Plan: Phase 5 (after core features working)

---

---

## Developer Handoff Checklist

If handing this off to a developer, they will need:

- [ ] This entity specification document (complete)
- [ ] REQUIREMENTS_TO_ARCHITECTURE.md (business logic scenarios)
- [ ] INSTALLATION_GUIDE.md (setup instructions)
- [ ] Access to DDEV environment
- [ ] Access to GitHub repo
- [ ] Access to InMotion hosting (for deployment)
- [ ] Test/sample data
- [ ] Clickatell API credentials (for SMS)
- [ ] Authorize.net credentials (for payments)
- [ ] Style guide / design mockups (if available)

---

---

## Next Steps

1. **Complete this document** - Fill in all [ADD MORE] sections
2. **Clarify questions** - Answer the questions listed above
3. **Review with team** - Get feedback from Andrew, board members
4. **Finalize entity design** - Sign off on structure
5. **Begin implementation** - Start with Phase 1 (League entity)

---

**End of Entity Specification Template**

*Remember: This is a living document. Update it as you discover new requirements or clarify existing ones.*
