# CC Soccer D11 - Requirements to Architecture Planning

**Planning Phase: Mapping Requirements to Entities, Services, Forms, and Reports**

Date: December 16, 2024

---

## Why This Planning Phase Matters

### Planning now saves massive time later:

**Without planning:**
```
Build Registration entity â†’ Realize you need Team reference
Build Team entity â†’ Realize you need Season reference
Build Season entity â†’ Realize Registration structure needs changes
Rebuild Registration â†’ Realize Commerce integration needs different approach
Refactor everything â†’ 2 weeks lost
```

**With planning:**
```
Map requirements â†’ Design entities â†’ See relationships clearly
Build once â†’ Everything fits together â†’ 3 days
```

**Time investment:** 4-6 hours of planning  
**Time saved:** Weeks of rework

---

## The Mapping Process

### 1. Requirements â†’ Entities (Data Model)

**From requirements, identify "nouns" that need to persist:**
- User (Drupal core - already exists)
- Registration (what we'll customize)
- Season
- Team
- Game
- Waitlist
- Override
- Product (Commerce - already exists)
- Order (Commerce - already exists)

---

### 2. Entities â†’ Relationships (Data Architecture)

**How do they connect?**
- User â†’ many Registrations
- Season â†’ many Registrations
- Season â†’ many Teams
- Team â†’ many Users (players)
- Game â†’ two Teams
- Season â†’ many Games
- Waitlist â†’ references User and Season
- Registration â†’ references Order

---

### 3. Requirements â†’ Services (Business Logic)

**From requirements, identify "verbs" that require complex logic:**
- TeamBalancerService (generate balanced teams)
- ScheduleGeneratorService (create round-robin schedule with time slot balancing)
- WaitlistManagerService (notification, expiration, queue management)
- CreditManagerService (calculate, issue, expire credits)
- NotificationService (email/SMS with templates)
- OverrideManagerService (create, expire, track overrides)

---

### 4. Requirements â†’ Forms (User Workflows)

**What do users/admins interact with?**

**Public Forms:**
- Registration checkout flow (Commerce)
- Group invitation acceptance
- Player profile update

**Admin Forms:**
- Schedule builder (input parameters, preview, manual edits)
- Roster builder (team suggestions, manual assignments)
- Season creation/management
- Waitlist management
- Override creation

---

### 5. Requirements â†’ Views/Reports (Data Display)

**What do people need to see?**
- Schedule display (calendar, list, iCal)
- Team rosters
- Registration lists
- Jersey distribution report
- Payment/deposit reports
- Waitlist queue
- Game status board

---

## Key Questions and Design Decisions

### Question 1: Where do invitations, groups, leagues, notifications, credits fit?

**Entities vs. Fields vs. Services discussion:**

- **Invitations:** NOT separate entity - fields on Registration (invited_by, invitation_status, group_id)
- **Groups:** NOT separate entity - logical collection using group_id field
- **League:** YES entity - different leagues have different settings
- **Notifications:** Use Message module (already installed)
- **Credits:** YES entity - for audit trail and history
- **Season/Tournament:** ONE entity (Season) with type field

---

### Question 2: Master Calendar for skip days?

**Options considered:**

**Option A: Master Calendar entity**
- Annual schedule with holidays/skip days
- Seasons reference this to inherit skip days
- More complex but reusable

**Option B: Skip days directly on Season**
- Multi-value date field on Season entity
- Simpler, more flexible
- Can still default from previous season

**Decision:** Start with Option B (skip_days on Season). Add Master Calendar later if needed.

---

### Question 3: User attributes - separate Player entity or extend User?

**Decision: Extend OOTB User entity with fields**

**Why:**
- User entity already exists
- Fields like "prefers goalie" belong to the person, not a role
- Roles handle permissions (Player, Captain, Board Member, etc.)
- Simpler data model

**User fields to add:**
- field_phone
- field_dob
- field_gender
- field_jersey_size
- field_skill_level (admin-set)
- field_self_score (player-entered)
- field_prefers_goalie
- field_credits_balance
- field_player_picture
- field_zip_code
- field_notification_preferences

---

### Question 4: Sub-objects - Field and Time Slot as entities?

**Decision: Just fields on Game entity, NOT separate entities**

**Why:**
- Field = list field with options ("Field 1", "Field 2", "Field 3")
- Time slot = integer field (1, 2, 3)
- No need for separate database tables
- Simpler queries
- Easier to manage

**Game entity structure:**
```
Game:
  - season (reference)
  - game_date (date)
  - game_time (time)
  - team_1 (team reference)
  - team_2 (team reference)
  - field (list field) â† Just a field
  - time_slot (integer) â† Just a field
  - week_number (integer)
  - status (list)
```

---

### Question 5: Content Types vs Entities?

**Content Types (Nodes) - Very Few:**

Use for pages that need revisions, publishing workflow, CMS features:
- League information pages ("About Coed League", "Rules")
- Help/FAQ pages
- News/announcements (if needed)

**That's it!**

**Entities - Everything Data-Related:**
- All data records (registrations, teams, games, etc.)
- Custom entities for business objects
- Performance matters (lots of queries)
- Clean data model

---

### Question 6: Registration - Object vs Process?

**Key insight: Registration is multiple things simultaneously**

#### 1. Registration Entity (DATA)
```php
Registration entity:
  - id: 123
  - player: User 456
  - season: Season 789
  - status: paid
  - jersey_size: L
  - commerce_order: Order 999
```
**The RECORD in the database.**

#### 2. Commerce Product (WHAT THEY BUY)

**Season Registration Product:**
- Product type: "Season Registration"
- Price: $120
- Attributes: Season (Fall 2025 Coed)

**Tournament Registration Product:**
- Product type: "Tournament Registration"
- Price: $80
- Different workflow

**Jersey Product:**
- Product type: "Jersey"
- Price: $25

**Different products â†’ Different workflows in checkout**

#### 3. Registration Form (USER INTERFACE)

**Commerce Checkout Panes:**
```
Cart:
  - Fall 2025 Coed Registration: $120
  - Jersey (L): $25

Checkout Step 1 - Player Info:
  â˜ Jersey size (if first time)
  â˜ Skill self-score (if first time)
  â˜ Prefers goalie? (if first time)
  
Checkout Step 2 - Group/Team:
  âŠ™ Register solo
  âŠ™ Create group (enter name, invite others)
  âŠ™ Join group (enter group code)
  
Checkout Step 3 - Payment:
  [Credit card form]
  Credits available: $30 â˜‘ Use credits
```

**Different for tournament:**
```
Checkout Step 2 - Team:
  âŠ™ Join existing team (select from dropdown)
  âŠ™ Create new team:
      Team name: ___________
      Pay deposit? â˜ (+$50)
```

#### 4. RegistrationService (BUSINESS LOGIC)

```php
class RegistrationService {
  
  public function processRegistration(Order $order, Season $season) {
    // 1. Create Registration entity
    $registration = Registration::create([
      'player' => $order->getCustomer(),
      'season' => $season,
      'status' => 'pending',
      'commerce_order' => $order,
    ]);
    
    // 2. Handle group logic
    if ($group_id) {
      $registration->set('group_id', $group_id);
    }
    
    // 3. Apply credits
    if ($use_credits) {
      $this->creditManager->applyCredits($registration);
    }
    
    // 4. Assign jersey if first registration
    if ($this->isFirstRegistration($user)) {
      $this->addJerseyToOrder($order);
    }
    
    $registration->save();
  }
  
  public function onPaymentComplete(Registration $registration) {
    // Update status
    $registration->set('status', 'paid');
    
    // Send confirmation
    $this->notificationService->sendConfirmation($registration);
    
    // Check if full, notify waitlist
    if ($this->isFull($season)) {
      $this->waitlistManager->notifyWaitlist($season);
    }
  }
}
```

---

## Entity Design - Final Decisions

### âœ… CUSTOM ENTITIES (We Build These - 7 Total)

All custom entities with full control over structure and behavior.

#### 1. League Entity

**Why:** Different leagues have different settings (team size, price, day, time slots)

**Fields:**
- name (string: "Coed League", "Men's League")
- type (list: coed, mens, coed35, womens)
- team_size_min (integer: 6)
- team_size_max (integer: 10)
- default_price (decimal: 120.00)
- day_of_week (list: Tuesday, Thursday)
- time_slots (multi-value list: 6:00pm, 7:00pm, 8:00pm)
- fields_available (multi-value list: Field 1, Field 2, Field 3)
- game_duration (integer: 60 minutes)
- description (text_long)

**Relationships:**
- League â†’ many Seasons (NOT Tournaments!)

**Examples:**
- "Coed League" (18 teams, Tuesdays, 3 fields, 3 time slots)
- "Men's League" (6 teams, Thursdays, 2 fields, 2 time slots)

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

---

#### 2. Season Entity

**Why:** Instance of a league at a point in time (regular weekly play)

**Fields:**
- league (entity reference to League - REQUIRED)
- name (string: "Fall 2025 Coed")
- start_date (date)
- end_date (date)
- registration_open (datetime)
- registration_close (datetime)
- max_players (integer)
- reserved_spots (integer - for waitlist overrides)
- price (commerce_price - can override league default)
- skip_days (multi-value date field: Thanksgiving, Christmas, etc.)
- schedule_generated (boolean)
- schedule_parameters (text_long - JSON of last used params)
- team_size_min (integer - can override league)
- team_size_max (integer - can override league)
- status (list: planned, registration_open, in_progress, completed)

**Relationships:**
- Season â†’ many Registrations
- Season â†’ many Teams
- Season â†’ many Games
- Belongs to: League

**Key Characteristics:**
- 10-12 week duration
- Admin-generated balanced teams
- Waitlist system with overrides
- Credits for cancellations
- Weekly schedule with skip days

---

#### 3. Tournament Entity

**Why:** Standalone tournament events - completely separate from league system

**Fields:**
- name (string: "Summer Kickoff 2025")
- start_date (date - usually 1 day)
- end_date (date - usually same as start)
- registration_open (datetime)
- registration_close (datetime)
- max_teams (integer - NOT max_players!)
- deposit_amount (decimal: 50.00 - for captains)
- registration_price (decimal: 80.00 - per player)
- age_requirement (integer: 18+, 21+, etc.)
- gender_requirement (list: coed, mens, open)
- location (string)
- fields_available (multi-value list)
- format (list: bracket, round_robin, pool_play)
- schedule_generated (boolean)
- schedule_parameters (text_long - JSON)
- status (list: planned, registration_open, in_progress, completed)
- description (text_long)

**Relationships:**
- Tournament â†’ many Registrations
- Tournament â†’ many Teams
- Tournament â†’ many Games
- Does NOT belong to League (standalone)

**Key Characteristics:**
- Single-day or weekend events
- Captain-led team formation (players form own teams)
- Deposit system for captains
- NO waitlist system
- NO credits system
- Own age/gender rules (not inherited from league)
- Open to non-season players

**Key Differences from Season:**
- Tracks max_teams (not max_players)
- Players form teams (not admin-generated)
- Captains pay deposit, get to name team, invite players
- No connection to league structure

---

#### 4. Team Entity

**Why:** Generated teams for a Season OR player-formed teams for a Tournament

**Fields:**
- season (entity reference to Season - optional, XOR with tournament)
- tournament (entity reference to Tournament - optional, XOR with season)
- name (string: "Team 1" for seasons, custom names for tournaments)
- players (multi-value entity reference to User)
- captain (entity reference to User - required for tournaments, null for seasons)
- color (list: Red, Blue, Green, Yellow, Orange, Purple)
- average_skill (decimal - calculated)
- average_age (decimal - calculated)
- goalie_count (integer - calculated)
- women_count (integer - calculated)
- created_date (datetime)
- notes (text_long - admin notes)

**Relationships:**
- Team â†’ Season OR Tournament (belongs to one, not both)
- Team â†’ many Users (has players)
- Team â†’ many Games (plays in)

**Validation:**
- Must have season XOR tournament (one or the other, not both, not neither)
- If tournament: must have captain
- If season: captain should be null

**Note:** For seasons, team names can be updated by Slofriendly role. For tournaments, captain chooses name.

---

#### 5. Game Entity

**Why:** Individual scheduled games (for Seasons or Tournaments)

**Fields:**
- season (entity reference to Season - optional, XOR with tournament)
- tournament (entity reference to Tournament - optional, XOR with season)
- game_date (date)
- game_time (time)
- team_1 (entity reference to Team)
- team_2 (entity reference to Team)
- field (list: Field 1, Field 2, Field 3) â† **Just a field, not entity**
- time_slot (integer: 1, 2, 3) â† **Just a field, not entity**
- week_number (integer - for seasons, bracket_round for tournaments)
- game_duration (integer - minutes, default 60)
- status (list: scheduled, cancelled, completed, postponed)
- cancellation_reason (text)
- referee (entity reference to User - optional, future)
- notes (text_long)

**Relationships:**
- Game â†’ Season OR Tournament (belongs to one, not both)
- Game â†’ Team (team_1)
- Game â†’ Team (team_2)

**Validation:**
- Must have season XOR tournament (one or the other, not both, not neither)
- team_1 and team_2 must belong to same season/tournament
- notes (text_long)

**Relationships:**
- Game â†’ Season (belongs to)
- Game â†’ Team (team_1)
- Game â†’ Team (team_2)

**Key Design Decision:** Field and Time Slot are just fields, not separate entities. Makes queries simpler.

---

#### 5. Credits Entity

**Why:** Track credit history for audit trail (earned when, expires when, used when) - **SEASONS ONLY**

**Note:** Credits are ONLY issued for season registrations, not tournaments.

**Fields:**
- user (entity reference to User)
- amount (decimal)
- type (list: earned, used, expired, adjusted)
- source (list: registration_cancelled, rain_out, admin_adjustment)
- source_registration (entity reference to Registration - must be season registration)
- source_order (entity reference to Order - if applicable)
- created (datetime)
- expires (datetime - 1 year from created)
- used_date (datetime - when used)
- status (list: active, used, expired)
- reason (text - explanation, e.g., "Cancelled after 2 of 12 games")

**Relationships:**
- Credits â†’ User (belongs to)
- Credits â†’ Registration (source, optional - must be season registration)
- Credits â†’ Order (source, optional)

**Additional:** User entity also has field_credits_balance (decimal) for quick lookup without querying all credit records.

**Credit Calculation Example:**
- Season registration cancelled: (total_paid / total_games) * games_remaining * 0.75 = credit amount
- Rain out: (registration_fee / total_games) * 0.75 per cancelled game

**Important:** Tournaments do NOT use credits - no cancellation credits, no rain out credits.

---

### âœ… ENTITIES WE EXTEND

#### 6. Registration Entity (Custom Entity)

**ARCHITECTURAL DECISION (Dec 18, 2024):** Built as custom entity rather than extending contrib Registration module.

**Why Custom Instead of Contrib:**
- Contrib Registration designed for "event registration" (attach registrations to host entities like nodes)
- Required fields: `entity_type_id` and `entity_id` (host entity pattern we don't need)
- Our needs: Direct entity references (`season` or `tournament`), not host attachment
- Simpler queries: `WHERE season = X` not `WHERE entity_type_id = 'season' AND entity_id = X`
- Full control over fields, validation, and business logic
- No fighting module assumptions or state management we don't use

**Commerce Integration:**
- Write custom `hook_commerce_checkout_complete()` instead of Commerce Registration module
- ~100 lines of clean, maintainable code for our exact workflow
- Handle jersey logic, group creation, credits, tournament deposits in our code

**Why:** Player registration for seasons OR tournaments. Full control over entity structure.

**Database Table:** `ccsoccer_registration`

**Entity Type:** Custom Content Entity

**Fields:**
- season (entity reference to Season - optional, XOR with tournament)
- tournament (entity reference to Tournament - optional, XOR with season)
- player (entity reference to User)
- jersey_size (list: S, M, L, XL, XXL - seasons only)
- skill_level (integer 1-10 - admin can adjust)
- self_score (integer 1-10 - player enters)
- prefers_goalie (boolean)
- group_id (string: "smith-family-fall-2025") â† **Seasons only, groups are logical, not entity**
- invited_by (entity reference to User - who sent invitation)
- invitation_status (list: none, pending, accepted, declined)
- team (entity reference to Team - assigned later)
- commerce_order (entity reference to Order)
- commerce_line_item (entity reference to Line Item)
- credits_used (decimal - seasons only)
- has_override (boolean - seasons only)
- override_expires (datetime - seasons only)
- override_notified (datetime - seasons only)
- registration_type (list: season_registration, tournament_registration)
- tournament_deposit_paid (boolean - tournaments only, captains only)
- is_captain (boolean - tournaments only)
- waiver_signed (boolean)
- waiver_date (datetime)
- status (list: pending, paid, active, cancelled, waitlist, expired)
- cancellation_date (datetime)
- cancellation_reason (text)
- notes (text_long - admin notes)

**Relationships:**
- Registration â†’ User (player)
- Registration â†’ Season OR Tournament (one, not both)
- Registration â†’ Team (assigned later)
- Registration â†’ Order (Commerce)

**Validation:**
- Must have season XOR tournament (one or the other, not both, not neither)
- If season: can use group_id, override, credits
- If tournament: deposit/captain logic applies, no override/waitlist/credits

**Key Design Decisions:**
- Groups are NOT entities - just group_id field that links registrations (seasons only)
- Invitations tracked via fields (invited_by, invitation_status)
- Can query "all registrations in group" by group_id
- Separate registration flows handled via registration_type field
- Flag module could also track invitation status if needed

---

#### 7. User Entity (Extend Drupal Core User)

**Why:** Players are users with additional attributes

**Core User fields:**
- uid
- name (username)
- mail (email)
- pass (password)
- roles (Player, Captain, Board Member, Slofriendly, Admin, Referee)
- status (active/blocked)
- created
- access (last login)

**Fields we add:**
- field_first_name (string)
- field_last_name (string)
- field_phone (telephone)
- field_dob (date)
- field_gender (list: Male, Female, Other, Prefer not to say)
- field_zip_code (string)
- field_jersey_size (list: S, M, L, XL, XXL)
- field_skill_level (integer 1-10 - admin-set, authoritative)
- field_self_score (integer 1-10 - player-entered)
- field_prefers_goalie (boolean)
- field_credits_balance (decimal)
- field_player_picture (image - required)
- field_notification_preferences (multi-value list: email, sms)
- field_emergency_contact_name (string)
- field_emergency_contact_phone (telephone)
- field_registration_history (entity reference to Registration - multi-value)

**Relationships:**
- User â†’ many Registrations
- User â†’ many Credits
- User â†’ many Teams (as player)
- User â†’ Team (as captain, tournaments)

**Roles handle permissions:**
- Player: Basic registration, profile
- Captain: Tournament-only, invitations, team management
- Board Member: Jersey reports, notifications, game status
- Slofriendly: Team naming, roster management, deposits
- Admin: Full access
- Referee: Future, notifications and scheduling

**Key Design Decision:** NO separate "Player" entity. User + fields + roles = everything we need.

---

### âœ… ENTITIES WE USE (Already Exist)

#### 8. Order Entity (Drupal Commerce)

**Provided by Commerce module**

**Key fields:**
- order_id
- order_number
- customer (User reference)
- order_items (Line Item references)
- total_price
- state (draft, completed, cancelled)
- payment_gateway
- payment_method
- placed (datetime)

**Used for:**
- Registration purchases
- Jersey purchases
- Tournament deposits
- Refunds

---

#### 9. Product Entity (Drupal Commerce)

**Provided by Commerce module**

**Product types we'll create:**
- Season Registration
- Tournament Registration
- Jersey
- Deposit (for tournaments)

**Key fields:**
- product_id
- title
- type
- price
- variations
- attributes (Season, Size, etc.)

---

#### 10. Message Entity (Message Module)

**Provided by Message module**

**Message templates we'll create:**
- registration_confirmation
- waitlist_spot_available
- override_notification
- group_invitation
- team_assignment
- schedule_published
- game_status_update
- payment_receipt
- credit_issued
- override_expiring_soon

**Used for:**
- Email notifications (via Symfony Mailer)
- SMS notifications (via custom Clickatell integration)
- Notification history tracking

---

### âŒ NOT ENTITIES

**Things that are NOT separate database tables:**

#### Invitations
- **What:** State/relationship between registrations
- **How:** Fields on Registration (invited_by, invitation_status, group_id)
- **Alternative:** Flag module could track if preferred

#### Groups
- **What:** Logical collection of registrations
- **How:** Registrations share same group_id field
- **Query:** "SELECT * FROM registration WHERE group_id = 'smith-family'"
- **Benefit:** Simple, flexible, no extra tables

#### Notifications
- **What:** Communications sent to users
- **How:** Message module (already installed)
- **Storage:** Message entities with templates

#### Schedule
- **What:** View/display of Game entities
- **How:** Views module queries games, displays as calendar/list/iCal
- **Benefit:** Schedule is dynamic view, not static data

#### Field (playing field)
- **What:** Location where game is played
- **How:** List field on Game entity
- **Values:** Field 1, Field 2, Field 3
- **Why not entity:** No additional attributes needed, just a location

#### Time Slot
- **What:** Which game time (6pm, 7pm, 8pm)
- **How:** Integer field on Game entity (1, 2, 3)
- **Why not entity:** Just an ordinal, no additional data

#### Master Calendar (Maybe Later)
- **What:** Annual schedule template with holidays
- **How (for now):** skip_days multi-value date field on Season
- **Future:** Could add Calendar entity if reuse across seasons is needed
- **Decision:** Start simple, add if needed

---

### âŒ CONTENT TYPES (Very Few)

**Use content types (nodes) ONLY for pages needing CMS features:**

**What needs to be a content type:**
- League information pages
  - "About Coed League"
  - "League Rules"
  - "How to Register"
- Help/FAQ pages
- News/Announcements (if wanted)

**Why:**
- Need revisions (edit content over time)
- Need publishing workflow (draft â†’ review â†’ publish)
- Content management features (SEO, menu integration)
- Occasional updates by staff

**That's it!** Everything else is entities.

---

## Registration Flow - How It All Works Together

### Complete Registration Process

```
1. USER ACTION:
   User clicks "Register for Fall 2025 Coed"
   
2. COMMERCE:
   Season Registration Product â†’ Add to cart
   Creates Line Item â†’ Creates Order (Commerce entities)
   
3. CHECKOUT FORM (Commerce Checkout Panes):
   
   Step 1 - Player Info (if first time):
   â”œâ”€ Jersey size dropdown
   â”œâ”€ Skill self-score (1-10)
   â””â”€ Prefers goalie? checkbox
   
   Step 2 - Group (season only):
   â”œâ”€ âŠ™ Register solo
   â”œâ”€ âŠ™ Create group: [group name] [invite emails]
   â””â”€ âŠ™ Join group: [group code]
   
   Step 2 - Team (tournament only):
   â”œâ”€ âŠ™ Join existing team [dropdown]
   â””â”€ âŠ™ Create new team: [team name] [pay deposit?]
   
   Step 3 - Credits:
   Available credits: $30.00
   â˜‘ Apply credits to order
   
   Step 4 - Payment:
   Credit card form (Authorize.net)
   
4. ON CHECKOUT COMPLETE (hook_commerce_checkout_complete):
   RegistrationService::processRegistration() triggers
   
5. REGISTRATION SERVICE CREATES ENTITY:
   $registration = Registration::create([
     'player' => $order->getCustomer(),
     'season' => $season,
     'status' => 'pending',
     'commerce_order' => $order,
     'group_id' => $group_id,
     'jersey_size' => $jersey_size,
     // etc
   ]);
   
6. ON PAYMENT COMPLETE (hook_commerce_payment_order_paid_in_full):
   RegistrationService::onPaymentComplete() triggers
   
7. REGISTRATION SERVICE UPDATES:
   - Sets status = 'paid'
   - Applies credits (if used)
   - Creates credit record (if cancellation)
   - Sends confirmation (NotificationService)
   - Checks capacity â†’ triggers waitlist if full
   - Sends invitations (if group creator)
   
8. DATABASE:
   Registration entity saved
   Credit entity created (if applicable)
   Message entity created (notification)
   Order updated (Commerce)
   
9. USER RECEIVES:
   - Email confirmation (via Message + Symfony Mailer)
   - Redirect to group invitation page (if created group)
   - Registration confirmation page
```

---

## Entity Relationships Diagram

```
User (Drupal Core + fields)
  â†“ has many
Registration (extend contrib)
  â”œâ”€ references â†’ Season
  â”œâ”€ references â†’ Team (assigned later)
  â”œâ”€ references â†’ Order (Commerce)
  â””â”€ has â†’ group_id (text field, not entity)

League (custom)
  â†“ has many
Season (custom)
  â”œâ”€ type = season OR tournament
  â”œâ”€ skip_days (multi-value dates)
  â”œâ”€ has many â†’ Registrations
  â”œâ”€ has many â†’ Teams
  â””â”€ has many â†’ Games

Team (custom)
  â”œâ”€ belongs to â†’ Season
  â”œâ”€ has many â†’ Users (players)
  â””â”€ plays in many â†’ Games

Game (custom)
  â”œâ”€ belongs to â†’ Season
  â”œâ”€ references â†’ Team (team_1)
  â”œâ”€ references â†’ Team (team_2)
  â”œâ”€ has field (list field, not entity)
  â””â”€ has time_slot (integer field, not entity)

Credits (custom)
  â”œâ”€ belongs to â†’ User
  â”œâ”€ source from â†’ Registration (optional)
  â””â”€ source from â†’ Order (optional)

Order (Commerce)
  â”œâ”€ belongs to â†’ User
  â””â”€ has many â†’ Line Items

Product (Commerce)
  â””â”€ types: Season Registration, Tournament Registration, Jersey, Deposit

Message (Message module)
  â”œâ”€ belongs to â†’ User
  â””â”€ references â†’ Season (in message fields)
```

---

## Services Layer - Business Logic

### Services We Need to Build

#### 1. RegistrationService
**Purpose:** Orchestrate registration process

**Methods:**
- `processRegistration(Order $order, Season $season)` - Create registration on checkout
- `onPaymentComplete(Registration $registration)` - Update status on payment
- `cancelRegistration(Registration $registration)` - Handle cancellation, issue credits
- `applyOverride(User $user, Season $season)` - Give reserved spot
- `isFirstRegistration(User $user)` - Check if needs jersey
- `getGroupMembers(string $group_id)` - Get registrations in group
- `canRegister(User $user, Season $season)` - Check capacity, prerequisites

---

#### 2. TeamBalancerService
**Purpose:** Generate balanced teams from registrations

**Methods:**
- `generateTeams(Season $season)` - Main team generation algorithm
- `calculateTeamScore(Team $team)` - Average skill, age
- `balanceGoalies(array $teams)` - Distribute goalies evenly
- `getSuggestions(Team $team)` - Suggest players to balance team
- `manualAssignment(User $user, Team $team)` - Admin override

**Algorithm:**
1. Get all paid registrations for season
2. Sort by skill level
3. Snake draft distribution (Team 1 â†’ Team N â†’ Team N â†’ Team 1)
4. Balance goalies
5. Check age distribution
6. Create Team entities
7. Update Registration entities with team assignment

---

#### 3. ScheduleGeneratorService
**Purpose:** Generate game schedule with round-robin + time slot balancing

**Methods:**
- `generateSchedule(Season $season, array $params)` - Main schedule generation
- `generateRoundRobin(array $teams)` - Create matchups
- `assignTimeSlots(array $games, Season $season)` - Distribute time slots fairly
- `balanceTimeSlots(array $games)` - Post-processing to even distribution
- `handleByeWeeks(array $games, $total_teams)` - If odd number of teams
- `applySkipDays(array $games, Season $season)` - Skip holidays

**Algorithm:**
1. Get teams for season
2. Generate round-robin matchups
3. Assign dates (start_date + skip_days)
4. Assign fields (distribute evenly)
5. Assign time slots (balance across season)
6. Create Game entities
7. Return schedule for preview

---

#### 4. WaitlistManagerService
**Purpose:** Manage waitlist queue and notifications

**Methods:**
- `addToWaitlist(User $user, Season $season)` - Add to queue
- `getWaitlistPosition(User $user, Season $season)` - Get position
- `notifyNextInLine(Season $season)` - Send notification with override
- `checkExpiredNotifications()` - Cron: Check for expired overrides
- `convertToRegistration(Override $override, Order $order)` - Convert waitlist to registration
- `removeFromWaitlist(User $user, Season $season)` - User removes self

**Workflow:**
1. Registration cancelled or season opens spots
2. notifyNextInLine() called
3. Create override with expiration (7 days)
4. Send notification (Message module)
5. If user registers â†’ convert
6. If expires â†’ notify next person

---

#### 5. CreditManagerService
**Purpose:** Calculate, issue, track, and expire credits

**Methods:**
- `calculateCancellationCredit(Registration $registration)` - Calculate credit amount
- `issueCredit(User $user, $amount, $source)` - Create Credit entity
- `applyCredits(Registration $registration, $amount)` - Use credits on registration
- `getUserBalance(User $user)` - Get available credits
- `expireCredits()` - Cron: Expire old credits (1 year)
- `getHistory(User $user)` - Get credit history

**Calculation Example:**
```php
// Registration cancelled mid-season
$games_total = 12;
$games_played = 5;
$registration_fee = 120;

$credit = ($registration_fee / $games_total) * ($games_total - $games_played) * 0.75;
// = (120 / 12) * 7 * 0.75
// = 10 * 7 * 0.75
// = $52.50
```

---

#### 6. NotificationService
**Purpose:** Send email/SMS notifications using Message module

**Methods:**
- `sendRegistrationConfirmation(Registration $registration)`
- `sendWaitlistNotification(User $user, Season $season, Override $override)`
- `sendGroupInvitation(User $inviter, array $emails, $group_id)`
- `sendTeamAssignment(Team $team)`
- `sendSchedulePublished(Season $season)`
- `sendGameStatusUpdate(Game $game)`
- `sendOverrideExpiring(Override $override)` - 24 hours before expiration
- `sendCreditIssued(Credits $credit)`

**Integration:**
- Uses Message module templates
- Sends via Symfony Mailer (email)
- Sends via custom Clickatell integration (SMS)
- Tracks notification history in Message entities

---

#### 7. OverrideManagerService
**Purpose:** Manage reserved spots and expiration

**Methods:**
- `createOverride(User $user, Season $season, $expires_days = 7)`
- `extendOverride(Registration $registration, $additional_days)`
- `revokeOverride(Registration $registration)`
- `checkExpirations()` - Cron: Check for expired overrides
- `sendExpiringReminders()` - Cron: 24 hours before expiration
- `convertOverride(Registration $registration)` - User completes registration

**Workflow:**
1. Admin or Waitlist triggers override
2. Create with expiration (default 7 days)
3. Send notification
4. Track in Registration (has_override, override_expires)
5. Cron checks daily for expirations
6. Send reminder 24 hours before
7. If expires â†’ revoke, notify next waitlist

---

## Admin Forms/Interfaces

### Forms We Need to Build

#### 1. Season Management Form
**Path:** `/admin/cc-soccer/seasons/add`
**Purpose:** Create and configure seasons

**Fields:**
- League (dropdown)
- Name (auto-generate or manual)
- Type (season or tournament)
- Start/End dates
- Registration open/close
- Max players, reserved spots
- Price (default from league, can override)
- Skip days (multi-value date picker)
- Team sizes (min/max)

---

#### 2. Schedule Builder Form
**Path:** `/admin/cc-soccer/season/{id}/build-schedule`
**Purpose:** Generate schedule with parameters

**Steps:**
1. Input Parameters:
   - Start date (default from season)
   - Number of games
   - Fields available
   - Time slots
   - Skip days (pre-filled from season)
   
2. Preview:
   - Show generated schedule
   - Highlight time slot distribution
   - Allow manual edits (swap games, change dates)
   
3. Confirm:
   - Save Game entities
   - Mark season.schedule_generated = TRUE

---

#### 3. Roster Builder Form
**Path:** `/admin/cc-soccer/season/{id}/build-roster`
**Purpose:** Generate teams with balance suggestions

**Steps:**
1. Generate:
   - Click "Generate Balanced Teams"
   - Algorithm runs
   - Show generated teams with stats
   
2. Review:
   - Show each team with:
     - Players list
     - Average skill
     - Average age
     - Goalie count
   - Suggestions for improving balance
   
3. Manual Adjustments:
   - Drag-drop players between teams
   - Re-calculate stats
   
4. Finalize:
   - Save Team entities
   - Update Registrations with team assignments
   - Send team assignment notifications

---

#### 4. Waitlist Management Form
**Path:** `/admin/cc-soccer/season/{id}/waitlist`
**Purpose:** View and manage waitlist queue

**Display:**
- Position, Name, Date Added, Status
- Actions: Remove, Notify (send override), Skip to next

**Actions:**
- Notify next in line (create override)
- Remove from waitlist
- Bulk notify (multiple overrides)

---

#### 5. Override Management Form
**Path:** `/admin/cc-soccer/overrides`
**Purpose:** Track and manage reserved spots

**Display:**
- User, Season, Created, Expires, Status
- Actions: Extend, Revoke, Nudge (send reminder)

---

## Views/Reports

### Views We Need to Create

#### 1. Schedule Display (Public)
**Displays:**
- Full Calendar view (FullCalendar module)
- List view (by date)
- Table view (by week)
- iCal feed (Date iCal module)
- PDF export (Entity Print module)

**Filters:**
- By season
- By team
- By field
- Date range

---

#### 2. Team Rosters (Public)
**Display:** List of teams with player names

**Filters:**
- By season
- By team

**Export:** PDF

---

#### 3. Registration List (Admin)
**Display:** Table of all registrations

**Fields:**
- Player name
- Season
- Status
- Jersey size
- Team (if assigned)
- Payment status
- Registration date

**Filters:**
- Season
- Status
- Has jersey
- Has team assignment

**Export:** CSV (Views Data Export)

---

#### 4. Jersey Distribution Report (Admin)
**Display:** List for jersey distribution

**Fields:**
- Player name
- Jersey size
- League (Coed/Men's)
- Day (Tuesday/Thursday)
- Already has jersey?
- Notes

**Filters:**
- Season
- League/Night
- Cancelled (exclude)

**Export:** Excel (Views Data Export)

---

#### 5. Payment Report (Admin)
**Display:** Financial summary

**Fields:**
- Player name
- Order number
- Amount paid
- Credits used
- Payment method
- Date
- Status

**Filters:**
- Date range
- Season
- Payment status

**Export:** CSV

---

#### 6. Waitlist Queue (Admin)
**Display:** Current waitlist

**Fields:**
- Position
- Player name
- Season
- Date added
- Status (waiting/notified/expired)
- Override expires

**Actions:**
- Notify
- Remove

---

## Clarified Business Logic Scenarios

### Reserved Spots & Waitlist Flow

**The complete lifecycle of how reserved spots work with waitlist:**

```
INITIAL STATE: Season with 100 max spots
â”œâ”€ 100 regular spots available
â”œâ”€ 0 reserved spots
â””â”€ 0 waitlist

REGISTRATION PHASE (Normal):
â”œâ”€ Players register (1-100)
â”œâ”€ Spots consumed from regular spots
â”œâ”€ At 100 registrations â†’ FULL
â””â”€ Further attempts â†’ Added to waitlist

CANCELLATION WHEN NO WAITLIST:
â”œâ”€ Player cancels
â”œâ”€ Check waitlist: Empty
â”œâ”€ Result: Opens 1 regular spot
â”œâ”€ First come, first serve
â””â”€ Status: 99 registered, 1 open spot, 0 reserved

CANCELLATION WHEN WAITLIST EXISTS (Key Logic):
â”œâ”€ Player cancels
â”œâ”€ Check waitlist: Has entries
â”œâ”€ Result:
â”‚   â”œâ”€ Do NOT open regular spot
â”‚   â”œâ”€ Increment reserved_spots (now 1 reserved)
â”‚   â”œâ”€ Grant override to waitlist position #1
â”‚   â”œâ”€ Set expiration (typically 1-2 days)
â”‚   â””â”€ Auto-notify waitlist person
â”œâ”€ Status: 99 registered, 0 open spots, 1 reserved spot
â””â”€ Reserved spot "held" for specific person

OVERRIDE REGISTRATION:
â”œâ”€ Waitlist person registers with override
â”œâ”€ Consumes reserved spot
â”œâ”€ Decrement reserved_spots (back to 0)
â”œâ”€ Status: 100 registered, 0 open, 0 reserved
â””â”€ Back to FULL

OVERRIDE EXPIRATION (Not Used):
â”œâ”€ Override expires (person didn't register)
â”œâ”€ Check waitlist: Are there more people?
â”‚   
â”‚   YES - More waitlist:
â”‚   â”œâ”€ Grant override to next person
â”‚   â”œâ”€ Auto-notify them
â”‚   â”œâ”€ Same reserved spot passes to them
â”‚   â””â”€ Reserved spot count stays at 1
â”‚   
â”‚   NO - Waitlist empty:
â”‚   â”œâ”€ Decrement reserved_spots (back to 0)
â”‚   â””â”€ Becomes regular open spot
â”‚
â””â”€ Original person loses opportunity

REGISTRATION WITHOUT OVERRIDE (When spots exist):
â”œâ”€ Player has no override
â”œâ”€ Regular spots available
â”œâ”€ Consumes regular spot
â””â”€ Normal registration flow

REGISTRATION WITHOUT OVERRIDE (When full):
â”œâ”€ Player has no override
â”œâ”€ No regular spots (even if reserved spots exist)
â”œâ”€ Cannot register
â””â”€ Added to waitlist

KEY RULE: NEVER exceed max spots (100)
â”œâ”€ Registered + Open + Reserved = 100 (always)
â”œâ”€ Reserved spots only for waitlist overrides
â”œâ”€ Overrides cannot bypass max capacity
â””â”€ System prevents over-registration
```

**This ensures:**
- Fair waitlist queue
- No race conditions
- Controlled registration when full
- Never exceed capacity

---

### Cancellation & Credits Calculation

**Complete cancellation flow with credit/refund logic:**

```
CANCELLATION BEFORE FIRST GAME:
â”œâ”€ Player requests cancellation
â”œâ”€ Check: Has season started?
â”‚   â””â”€ NO - Before first game
â”œâ”€ Payment breakdown:
â”‚   â”œâ”€ Registration: $120
â”‚   â””â”€ Jersey: $25
â”‚   â””â”€ Total paid: $145
â”œâ”€ Jersey handling:
â”‚   â”œâ”€ Can return jersey (if before season starts)
â”‚   â””â”€ Full refund includes jersey
â”œâ”€ Refund options:
â”‚   â”œâ”€ Default: Store credit ($145)
â”‚   â”œâ”€ On request: Full refund ($145)
â”‚   â””â”€ Special cases: Moving/emergency â†’ Refund
â”œâ”€ Credit properties if chosen:
â”‚   â”œâ”€ Amount: $145.00
â”‚   â”œâ”€ Issued: Today
â”‚   â”œâ”€ Expires: 1 year from today
â”‚   â””â”€ Reason: "Cancellation before season start"
â””â”€ Update registration status: cancelled

CANCELLATION AFTER GAMES PLAYED:
â”œâ”€ Player requests cancellation
â”œâ”€ Check: Has season started?
â”‚   â””â”€ YES - Games played
â”œâ”€ Payment breakdown:
â”‚   â””â”€ Total paid: $145 (reg + jersey)
â”œâ”€ Season details:
â”‚   â”œâ”€ Total games: 12
â”‚   â”œâ”€ Games played: 2
â”‚   â””â”€ Games remaining: 10
â”œâ”€ Credit calculation:
â”‚   Formula: (total_paid / total_games) * games_remaining * 0.75
â”‚   = ($145 / 12) * 10 * 0.75
â”‚   = $12.08 * 10 * 0.75
â”‚   = $90.60
â”œâ”€ Jersey handling:
â”‚   â””â”€ Player keeps jersey (not returned mid-season)
â”œâ”€ Credit issued:
â”‚   â”œâ”€ Amount: $90.60
â”‚   â”œâ”€ Issued: Today
â”‚   â”œâ”€ Expires: 1 year from today
â”‚   â””â”€ Reason: "Cancellation after 2 of 12 games"
â”œâ”€ Refund option:
â”‚   â””â”€ On specific request: Issue refund instead of credit
â””â”€ Update registration status: cancelled

CANCELLATION IMPACT ON SPOTS:
â”œâ”€ Check: Is there a waitlist?
â”‚   YES:
â”‚   â”œâ”€ Increment reserved_spots
â”‚   â””â”€ Grant override to next waitlist person
â”‚   
â”‚   NO:
â”‚   â””â”€ Opens regular spot
â”‚
â””â”€ (See Reserved Spots flow above)

BOARD MEMBER DISCRETION:
â”œâ”€ Emergency situations
â”œâ”€ Moving before season
â”œâ”€ Special circumstances
â””â”€ Can override credit vs refund policy
```

**Example calculations:**

```
Scenario 1: Early cancellation
Paid: $145 | Games: 12 | Played: 0 | Remaining: 12
Credit: ($145 / 12) * 12 * 0.75 = $108.75 (or full refund $145)

Scenario 2: Mid-season cancellation
Paid: $145 | Games: 12 | Played: 5 | Remaining: 7
Credit: ($145 / 12) * 7 * 0.75 = $63.44

Scenario 3: Late cancellation
Paid: $145 | Games: 12 | Played: 10 | Remaining: 2
Credit: ($145 / 12) * 2 * 0.75 = $18.13

Scenario 4: Registration only (no jersey)
Paid: $120 | Games: 12 | Played: 3 | Remaining: 9
Credit: ($120 / 12) * 9 * 0.75 = $67.50
```

---

### Override Lifecycle & Timing

**Complete override process from grant to resolution:**

```
ADMIN GRANTS OVERRIDE:
â”œâ”€ Admin selects user from waitlist
â”œâ”€ Admin sets expiration date
â”‚   â”œâ”€ Typical: 1-2 days
â”‚   â”œâ”€ Admin discretion: Can be longer/shorter
â”‚   â””â”€ Stored as datetime field
â”œâ”€ System actions:
â”‚   â”œâ”€ Update registration: has_override = TRUE
â”‚   â”œâ”€ Set override_expires = [date]
â”‚   â”œâ”€ Reserved spot stays reserved
â”‚   â””â”€ Auto-send notification
â”œâ”€ Notification content:
â”‚   â”œâ”€ "You have been granted priority registration"
â”‚   â”œâ”€ Season details
â”‚   â”œâ”€ Expiration date/time
â”‚   â”œâ”€ Direct registration link
â”‚   â””â”€ Urgency implied (short window)
â””â”€ NO reminder sent (window too short)

PLAYER USES OVERRIDE (Success Path):
â”œâ”€ Player clicks registration link
â”œâ”€ Registration system checks:
â”‚   â”œâ”€ Has valid override? YES
â”‚   â”œâ”€ Override expired? NO
â”‚   â””â”€ Proceed to checkout
â”œâ”€ Registration completes:
â”‚   â”œâ”€ Override marked as: used
â”‚   â”œâ”€ Override_expires cleared
â”‚   â”œâ”€ Reserved spot consumed
â”‚   â”œâ”€ Registration status: paid/active
â”‚   â””â”€ Confirmation sent
â””â”€ Waitlist position released

OVERRIDE EXPIRES (Not Used):
â”œâ”€ Cron job runs (hourly check)
â”œâ”€ Find overrides where:
â”‚   â””â”€ override_expires < NOW AND status != used
â”œâ”€ For each expired override:
â”‚   â”œâ”€ Mark override as: expired
â”‚   â”œâ”€ Clear override_expires
â”‚   â”œâ”€ Check: More people on waitlist?
â”‚   â”‚   
â”‚   â”‚   YES:
â”‚   â”‚   â”œâ”€ Get next waitlist position
â”‚   â”‚   â”œâ”€ Grant new override to that person
â”‚   â”‚   â”œâ”€ Auto-notify new person
â”‚   â”‚   â””â”€ Reserved spot passes to them
â”‚   â”‚   
â”‚   â”‚   NO:
â”‚   â”‚   â”œâ”€ Decrement reserved_spots
â”‚   â”‚   â”œâ”€ Becomes regular open spot
â”‚   â”‚   â””â”€ First come, first serve
â”‚   â”‚
â”‚   â””â”€ Original person removed from consideration
â””â”€ Admin notification (daily summary)

MANUAL OVERRIDE ACTIONS (Admin):
â”œâ”€ Extend override:
â”‚   â”œâ”€ Admin adds more days
â”‚   â”œâ”€ Update override_expires
â”‚   â””â”€ Optional: Re-notify player
â”œâ”€ Revoke override:
â”‚   â”œâ”€ Admin cancels override
â”‚   â”œâ”€ Clear override fields
â”‚   â”œâ”€ Pass to next waitlist or open spot
â”‚   â””â”€ Notify player (optional)
â””â”€ Nudge:
    â”œâ”€ Manual reminder to player
    â””â”€ "Your override expires soon"

TYPICAL TIMELINE:
Day 1, 9am:  Player cancels registration
Day 1, 9:05: System grants override to waitlist #1
Day 1, 9:06: Notification sent
Day 2, 9am:  Override expires (24 hours)
Day 2, 9:05: System grants override to waitlist #2
Day 2, 9:06: Notification sent to person #2
```

**Fast turnaround ensures:**
- Spots fill quickly before games
- Urgency motivates quick decision
- Fair rotation through waitlist
- Minimal admin intervention

---

### Tournament Registration Flows

**Three distinct paths for tournament registration:**

#### **Flow 1: Captain Registration (with optional deposit)**

```
PLAYER PATH:
â”œâ”€ Browse to tournament registration
â”œâ”€ Add "Tournament Registration" to cart ($80)
â”œâ”€ Checkout begins
â”œâ”€ Step 1: Player Info
â”‚   â”œâ”€ Waiver acceptance (required)
â”‚   â””â”€ Age check (18+)
â”œâ”€ Step 2: Team Formation
â”‚   â”œâ”€ Option shown: "Pay tournament deposit ($50)"
â”‚   â”œâ”€ Checkbox: â˜‘ "I want to form a team (pay deposit)"
â”‚   â””â”€ If checked:
â”‚       â”œâ”€ Add "Tournament Deposit" product to cart
â”‚       â”œâ”€ Show field: "Team Name" (required)
â”‚       â””â”€ Cart total: $130 ($80 + $50)
â”œâ”€ Step 3: Credits (if available)
â”‚   â””â”€ Apply available credits
â”œâ”€ Step 4: Payment
â”‚   â”œâ”€ Discount applied (if user has discount %)
â”‚   â””â”€ Complete payment
â””â”€ On Success:
    â”œâ”€ Registration created (status: paid)
    â”œâ”€ If deposit paid:
    â”‚   â”œâ”€ User marked as captain
    â”‚   â”œâ”€ Group created with team_name
    â”‚   â”œâ”€ User assigned as group captain
    â”‚   â””â”€ Email sent to slofriendly@ccsoccer.com:
    â”‚       "Captain [name] formed team '[team name]' 
    â”‚        for [tournament]"
    â”œâ”€ Confirmation email sent to player
    â””â”€ Redirect to group management page
        â”œâ”€ Can invite players to team
        â””â”€ Can manage roster

ADMIN/SLOFRIENDLY ACTIONS:
â”œâ”€ Receives captain notification
â”œâ”€ Can review team names
â”œâ”€ Can rename if inappropriate
â””â”€ Can see deposit report
```

#### **Flow 2: Player Joining Existing Team**

```
PLAYER PATH:
â”œâ”€ Browse to tournament registration
â”œâ”€ Add "Tournament Registration" to cart ($80)
â”œâ”€ Checkout begins
â”œâ”€ Step 1: Player Info
â”‚   â”œâ”€ Waiver acceptance (required)
â”‚   â””â”€ Age check (18+)
â”œâ”€ Step 2: Team Selection
â”‚   â”œâ”€ Show dropdown: "Select a team"
â”‚   â”œâ”€ List populated from:
â”‚   â”‚   â””â”€ Groups where has_captain = TRUE for this tournament
â”‚   â”œâ”€ Player selects team from dropdown
â”‚   â””â”€ Or: "I'm a free agent" (see Flow 3)
â”œâ”€ Step 3: Credits (if available)
â”‚   â””â”€ Apply available credits
â”œâ”€ Step 4: Payment
â”‚   â”œâ”€ Discount applied (if applicable)
â”‚   â””â”€ Complete payment
â””â”€ On Success:
    â”œâ”€ Registration created (status: paid)
    â”œâ”€ Added to selected team's group
    â”œâ”€ Captain can see them in roster
    â”œâ”€ Captain can remove if mistake
    â”œâ”€ Confirmation email sent
    â””â”€ Redirect to team page (view only)

CAPTAIN MANAGEMENT:
â”œâ”€ Captain sees new player in roster
â”œâ”€ Can remove if mistake:
â”‚   â””â”€ Player still registered but not on team
â”‚   â””â”€ Player can select different team
â””â”€ Can send team notifications
```

#### **Flow 3: Free Agent Registration (TBD)**

```
PLAYER PATH:
â”œâ”€ Browse to tournament registration
â”œâ”€ Add "Tournament Registration" to cart ($80)
â”œâ”€ Checkout begins
â”œâ”€ Step 1: Player Info
â”‚   â”œâ”€ Waiver acceptance (required)
â”‚   â””â”€ Age check (18+)
â”œâ”€ Step 2: Team Selection
â”‚   â””â”€ Select: "I'm a free agent"
â”œâ”€ Step 3-4: Credits & Payment (normal)
â””â”€ On Success:
    â”œâ”€ Registration created (status: paid, team: NULL)
    â”œâ”€ Marked as: free_agent = TRUE
    â””â”€ Confirmation email (different content)

ADMIN HANDLING (To Be Determined):
Options to consider:
â”œâ”€ Option A: Free agent "pool"
â”‚   â”œâ”€ Admin view of all free agents
â”‚   â”œâ”€ Can manually assign to teams
â”‚   â””â”€ Or form teams from free agents
â”œâ”€ Option B: Waitlist-style
â”‚   â”œâ”€ Free agents can request to join teams
â”‚   â”œâ”€ Teams can browse free agents
â”‚   â””â”€ Mutual acceptance required
â”œâ”€ Option C: Auto-assignment
â”‚   â”œâ”€ System forms teams from free agents
â”‚   â”œâ”€ Based on skill balancing
â”‚   â””â”€ Notifies when team is formed
â””â”€ DECISION NEEDED: Which approach to implement

NOTE: Free agent handling doesn't block current design.
Will implement after core flows are working.
```

**Key Differences Between Flows:**

| Aspect | Captain | Joining Team | Free Agent |
|--------|---------|--------------|------------|
| Deposit | Optional ($50) | No | No |
| Team Name | Required if deposit | Select from dropdown | None |
| Group Role | Captain | Member | None (pending) |
| Can Invite | Yes | No | No |
| Admin Notify | Yes (slofriendly) | No | No |
| Team Shown | Own team | Selected team | TBD |

---

### Permanent Override Logic

**User-level bypass for age/gender restrictions:**

```
USER FIELD:
â”œâ”€ field_permanent_override (boolean)
â”œâ”€ Set by: Admin only
â”œâ”€ Default: FALSE
â””â”€ Purpose: Allow board members to bypass restrictions

REGISTRATION CHECKS (in order):

1. CAPACITY CHECK:
   â”œâ”€ Check: spots_remaining > 0 OR has_override
   â”œâ”€ Applies to: EVERYONE (including permanent_override users)
   â””â”€ Cannot bypass: Max capacity is hard limit

2. AGE CHECK:
   â”œâ”€ Coed League: 18+ required
   â”œâ”€ Mens League: 35+ required
   â”œâ”€ Check: user.dob
   â”œâ”€ If permanent_override = TRUE:
   â”‚   â””â”€ BYPASS age check
   â””â”€ Else:
       â””â”€ Enforce age requirement

3. GENDER CHECK:
   â”œâ”€ Mens 35+ League: Male required
   â”œâ”€ Check: user.gender
   â”œâ”€ If permanent_override = TRUE:
   â”‚   â””â”€ BYPASS gender check
   â””â”€ Else:
       â””â”€ Enforce gender requirement

4. PROCEED TO CHECKOUT

USE CASES:

Board Member - Women in Mens League:
â”œâ”€ Board member (female) wants to register for Mens 35+
â”œâ”€ permanent_override = TRUE (set by admin)
â”œâ”€ Age check: BYPASSED
â”œâ”€ Gender check: BYPASSED
â”œâ”€ Capacity check: Still enforced
â””â”€ Can register normally

Special Exception Player:
â”œâ”€ Player has unique circumstance
â”œâ”€ Admin grants permanent_override
â”œâ”€ Example: 17-year-old exceptional player for 18+ league
â”œâ”€ Business rules bypassed
â””â”€ Capacity still enforced

Normal Player:
â”œâ”€ permanent_override = FALSE (default)
â”œâ”€ All checks enforced:
â”‚   â”œâ”€ Capacity: Must have spot or override
â”‚   â”œâ”€ Age: Must meet age requirement
â”‚   â””â”€ Gender: Must match gender requirement
â””â”€ Standard flow
```

**Important Notes:**
- Permanent override is user-level flag, not seasonal
- Still subject to capacity limits (never exceed max)
- Admin discretion for granting
- Should be rare exceptions
- Audit log shows when used

---

### Group Management & Roster Locking

**How groups work with roster generation timing:**

```
GROUP CREATION:
â”œâ”€ Only REGISTERED players can create groups
â”œâ”€ Player completes registration
â”œâ”€ On success, redirect to group page
â”œâ”€ Can create group:
â”‚   â”œâ”€ Enter group name (optional)
â”‚   â”œâ”€ Add emails to invite
â”‚   â””â”€ System generates: group_id (e.g., "smith-fall-2025")
â”œâ”€ Invitations sent to emails
â””â”€ Group status: active, unlocked

GROUP JOINING:
â”œâ”€ Invitee receives email with link
â”œâ”€ Clicks link, taken to registration
â”œâ”€ Registers normally
â”œâ”€ On success:
â”‚   â”œâ”€ Registration.group_id = [group_id]
â”‚   â””â”€ Joins group automatically
â””â”€ Group creator can see members

BEFORE ROSTER LOCK (Flexible Period):
â”œâ”€ Groups can add/remove members
â”œâ”€ Invitations can be sent/cancelled
â”œâ”€ Members can leave groups
â”œâ”€ New registrations can join groups
â”œâ”€ Late registrations with override:
â”‚   â””â”€ Can create or join groups normally
â””â”€ All group functions active

ADMIN RUNS ROSTER BUILDER:
â”œâ”€ Admin goes to: /admin/cc-soccer/season/{id}/build-roster
â”œâ”€ System checks: All registrations paid?
â”œâ”€ System generates teams:
â”‚   â”œâ”€ Groups treated as single unit
â”‚   â”œâ”€ Groups placed into teams first
â”‚   â”œâ”€ Individual players fill remaining spots
â”‚   â””â”€ Balance skill/age/gender
â”œâ”€ Admin reviews and approves
â””â”€ On approval: ROSTER LOCKS

AFTER ROSTER LOCK (Rigid Period):
â”œâ”€ Groups cannot change membership
â”œâ”€ Teams are set
â”œâ”€ Team assignments visible to players
â”œâ”€ Late registrations with override:
â”‚   â”œâ”€ Register as individual (no group)
â”‚   â”œâ”€ Not assigned to team automatically
â”‚   â”œâ”€ Admin manually assigns to team
â”‚   â””â”€ Or marks as substitute
â”œâ”€ Group functions disabled:
â”‚   â””â”€ Cannot send invitations
â”‚   â””â”€ Cannot leave group
â”‚   â””â”€ Cannot join group
â””â”€ Teams can play

SCENARIO: Late Override After Lock

Timeline:
Day 1: Roster generated and locked
Day 3: Player cancels
Day 3: Waitlist person given override
Day 4: Override person registers

What happens:
â”œâ”€ Registration created (status: paid)
â”œâ”€ group_id = NULL (cannot join groups)
â”œâ”€ team = NULL (not auto-assigned)
â”œâ”€ Admin sees unassigned player
â”œâ”€ Admin options:
â”‚   â”œâ”€ Manually assign to team needing player
â”‚   â”œâ”€ Mark as substitute
â”‚   â””â”€ Or leave unassigned until needed
â””â”€ Groups unaffected (stay locked)
```

**Timeline Summary:**

```
Registration Opens
    â†“
Players Register & Form Groups (UNLOCKED)
    â†“
Registration Closes
    â†“
Admin Reviews Registrations
    â†“
Admin Runs Roster Builder
    â†“
ROSTER LOCKS â† Key Moment
    â†“
Groups Frozen (no changes)
    â†“
Schedule Generated
    â†“
Season Starts
```

---

### Discount Display at Checkout

**Complete checkout cart summary with discount breakdown:**

```
CART DISPLAY:

â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
â”‚ Your Cart                                   â”‚
â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¤
â”‚ Fall 2025 Coed Registration        $120.00  â”‚
â”‚ Jersey (Large)                      $25.00  â”‚
â”‚                                    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”‚
â”‚ Subtotal                           $145.00  â”‚
â”‚                                             â”‚
â”‚ Discount (10%)                     -$14.50  â”‚  â† USER DISCOUNT %
â”‚ Credits Applied                    -$30.00  â”‚  â† FROM CREDIT BALANCE
â”‚                                    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”‚
â”‚ Total Due                          $100.50  â”‚
â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜

[Proceed to Payment]
```

**Calculation Order:**
1. **Subtotal:** Sum of all products
2. **Discount:** Subtotal Ã— user.discount_percent
3. **Credits:** Manually selected amount (up to balance)
4. **Total:** Subtotal - Discount - Credits

**User Discount Field:**
- Location: User entity, field_discount_percent
- Type: Integer (percentage: 0-100)
- Examples:
  - Board members: 100% (pay $0)
  - Referral discount: 10%
  - Special promo: 15%
- Applied to: All products in cart
- Set by: Admin only

**Display Rules:**
- Always show subtotal
- Only show discount line if user.discount_percent > 0
- Only show credits line if user has credits AND chooses to use them
- Always show total due
- If total = $0: Skip payment step, auto-complete order

**Board Member Checkout (100% discount):**
```
â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
â”‚ Your Cart                                   â”‚
â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¤
â”‚ Fall 2025 Coed Registration        $120.00  â”‚
â”‚ Jersey (Large)                      $25.00  â”‚
â”‚                                    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”‚
â”‚ Subtotal                           $145.00  â”‚
â”‚                                             â”‚
â”‚ Board Member Discount (100%)      -$145.00  â”‚
â”‚                                    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”‚
â”‚ Total Due                            $0.00  â”‚
â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜

Registration complete - no payment needed.
```

**Implementation:**
- Custom Commerce checkout pane: DiscountDisplayPane
- Calculates and displays discount
- Modifies order total before payment
- Shows in cart review and confirmation email

---

## Summary: Complete Architecture

### Custom Entities (5)
1. League
2. Season (includes tournaments)
3. Team
4. Game
5. Credits

### Custom Entities (6)
1. League
2. Season
3. Tournament (standalone, not tied to leagues)
4. Team
5. Game
6. Credits (seasons only)

### Extended Entities (2)
7. Registration (extend contrib, supports season OR tournament)
8. User (add fields)

### Existing Entities (3)
9. Order (Commerce)
10. Product (Commerce)
11. Message (Message module)

### Services (8+)
1. SeasonRegistrationService
2. TournamentRegistrationService
3. TeamBalancerService (seasons)
4. TournamentTeamManager (captain-led)
5. ScheduleGeneratorService
6. WaitlistManagerService (seasons only)
7. CreditManagerService (seasons only)
8. NotificationService
9. OverrideManagerService (seasons only)

### Admin Forms (6)
1. League Management
2. Season Management
3. Tournament Management
4. Schedule Builder (seasons)
5. Roster Builder (seasons)
6. Waitlist Management (seasons)
7. Override Management (seasons)

### Views/Reports (6)
1. Schedule Display
2. Team Rosters
3. Registration List
4. Jersey Distribution Report
5. Payment Report
6. Waitlist Queue

### Content Types (Minimal)
- League info pages
- Help/FAQ pages
- News (optional)

---

## Key Architectural Principles

### 1. Entities for Data, Not Concepts
- Groups are not entities (logical via group_id field)
- Invitations are not entities (state via fields)
- Schedule is not entity (view of games)
- Field/Time Slot are fields, not entities

### 2. Extend When Possible, Build When Necessary
- Registration: Extend contrib (saves time, gets updates)
- User: Extend core (natural fit)
- Season/Team/Game: Build custom (no good contrib exists)

### 3. Services for Complex Logic
- Algorithms (team balancing, scheduling)
- Multi-step workflows (registration process)
- Cross-entity operations (credits, notifications)

### 4. Commerce for Money
- Products for what they buy
- Orders for purchases
- Checkout panes for custom collection
- Hooks to integrate with our entities

### 5. Message for Notifications
- Templates in UI (easy to modify)
- History tracking (who was notified when)
- Multi-channel (email + SMS)
- Symfony Mailer handles delivery

### 6. One Entity, Multiple Purposes
- Season handles both seasons AND tournaments (type field)
- Registration handles both types (fields differ based on type)
- Cleaner than separate entities for similar concepts

---

## Next Steps

### Phase 1: Core Data Model (Week 1-2)
1. Create League entity
2. Create Season entity
3. Extend Registration entity
4. Add fields to User entity
5. Create database schema

### Phase 2: Registration Flow (Week 3-4)
1. Create Commerce products
2. Build checkout panes
3. Create RegistrationService
4. Integrate with Commerce
5. Test registration process

### Phase 3: Team & Schedule (Week 5-6)
1. Create Team entity
2. Create Game entity
3. Build TeamBalancerService
4. Build ScheduleGeneratorService
5. Build admin forms

### Phase 4: Notifications & Waitlist (Week 7-8)
1. Set up Message templates
2. Build NotificationService
3. Build WaitlistManagerService
4. Build OverrideManagerService
5. Test workflows

### Phase 5: Reports & Polish (Week 9-10)
1. Create Views
2. Build admin interfaces
3. Build CreditManagerService
4. Testing and refinement
5. Documentation

---

**This planning document captures the complete architectural thinking and will guide all development going forward.**
