# CC Soccer D11 - Session Handoff
**Date:** February 21, 2026
**Session:** User Account Validation & Age Enforcement
**Branch:** main

---

## Summary

Implemented comprehensive user account validation and age enforcement for the CC Soccer D11 Drupal site. Registration and profile edit forms are now simplified with required fields enforced, username restrictions, phone validation, 18+ minimum age on account creation, and per-season minimum age enforcement (blocking underage players from registering for age-restricted seasons like Men's 35+). New users are now redirected to their profile after first password set.

---

## What Was Completed

### 1. Registration Form — Simplified & Required Fields

**File:** `ccsoccer.module` — `ccsoccer_form_user_register_form_alter()`

Hidden unnecessary fields from the registration form (consolidated into a single `$hidden_fields` array): `field_skill_level`, `field_self_score`, `field_credits_balance`, `field_discount_percent`, `field_has_jersey`, `user_picture`, `field_jersey_size`, `field_email_visible`, `field_phone_visible`, `field_zip_code`, `contact`.

Made key fields required: First Name, Last Name, DOB, Gender.

Username restricted to alphanumeric characters and @ sign only (regex validation).

Notification preference description repositioned above the dropdown via `#description_display = 'before'`.

### 2. Profile Edit Form — Required Fields & Hidden Fields

**File:** `ccsoccer.module` — `ccsoccer_form_user_form_alter()`

Restructured from early-return-for-admins to conditional block so required fields and validation apply to all users.

Hidden for non-admins: `field_skill_level`, `field_self_score`, `field_credits_balance`, `field_discount_percent`, `field_has_jersey`, `user_picture`.

Hidden for all users: `field_email_visible`, `field_phone_visible`, `timezone`, `language`.

Made required for all users: First Name, Last Name, DOB, Gender.

### 3. Age Validation — 18+ Account Minimum

**File:** `ccsoccer.module`

Added `ccsoccer_user_register_validate()` — enforces alphanumeric+@ username and 18+ minimum age on registration.

Added `ccsoccer_user_profile_validate()` — enforces 18+ minimum age on profile edit.

Added `_ccsoccer_parse_dob_value()` helper — handles DrupalDateTime objects, arrays (datetime widget returns `['date' => '...', 'time' => '']`), and strings. This was needed because the datetime widget's return format caused a `DateTime::__construct()` type error.

### 4. Phone Number Validation

**File:** `ccsoccer.module` — `_ccsoccer_validate_phone_for_notifications()`

Phone is conditionally required when notification preference is set to "text" or "both" (email and text).

Format validation strips formatting characters (spaces, dashes, parentheses, dots, plus sign) and validates 10 digits (US standard) or 11 digits with leading 1.

Called from both registration and profile edit validation handlers.

### 5. First-Login Redirect

**File:** `ccsoccer.module` — `ccsoccer_form_user_form_alter()` + `ccsoccer_user_first_password_redirect()`

Detects `pass-reset-token` query parameter (present when user clicks one-time login link from email). After saving their password, redirects to `/user/{uid}` (profile page) instead of staying on the edit form.

### 6. Season Minimum Age Field

**File:** `src/Entity/Season.php`

Added `minimum_age` base field definition (integer, default 18, required, weight 11) and `getMinimumAge()` accessor method.

### 7. Age Enforcement on Season/Tournament Registration

**File:** `src/Controller/RegistrationController.php` (THE CRITICAL FILE)

This is the actual registration flow — the `/register` page renders season cards with direct links to `/register/season/{season}` which calls `addSeasonToCart()`. The `RegistrationSelectForm` is NOT used in the primary flow.

Added `checkUserAge($minimum_age, $entity)` protected method that loads user DOB, computes age using the season/tournament `start_date` as reference date, and returns an error message or `TRUE`.

Added age check in `addSeasonToCart()` using `$season->getMinimumAge()`.

Added age check in `addTournamentToCart()` using the tournament's `age_requirement` field (fallback to 18).

**File:** `src/Form/RegistrationSelectForm.php` — secondary safeguard with age validation in `validateForm()`.

### 8. Database Update Hooks

**File:** `ccsoccer.install`

`ccsoccer_update_9049()` — Installs `minimum_age` field on Season entity, sets all existing seasons to 18.

`ccsoccer_update_9050()` — Finds seasons whose league name contains "35+" and sets `minimum_age` to 35. (Result: set 21 Men's 35+ seasons to 35.)

### 9. Config Changes (Direct Edits to config/sync)

**File:** `config/sync/field.field.user.user.field_dob.yml` — changed `required: false` to `required: true`.

**File:** `config/sync/core.entity_form_display.user.user.default.yml` — moved `contact` from content section to hidden section.

Note: These were edited directly in config/sync, so `ddev drush cex -y` shows no differences for them (they're already exported).

---

## Files Summary

### Modified Files
| File | Changes |
|------|---------|
| `web/modules/custom/ccsoccer/ccsoccer.module` | Registration form alter (hidden fields, required fields, username validation, notification description), profile edit form alter (hidden fields, required fields, first-login redirect), `_ccsoccer_parse_dob_value()` helper, `ccsoccer_user_register_validate()`, `ccsoccer_user_profile_validate()`, `_ccsoccer_validate_phone_for_notifications()`, `ccsoccer_user_first_password_redirect()` |
| `web/modules/custom/ccsoccer/src/Entity/Season.php` | Added `minimum_age` base field definition + `getMinimumAge()` accessor |
| `web/modules/custom/ccsoccer/src/Controller/RegistrationController.php` | Added `checkUserAge()` method; age checks in `addSeasonToCart()` and `addTournamentToCart()` |
| `web/modules/custom/ccsoccer/src/Form/RegistrationSelectForm.php` | Age validation in `validateForm()` (secondary safeguard) |
| `web/modules/custom/ccsoccer/ccsoccer.install` | Added `ccsoccer_update_9049()` (install minimum_age field) and `ccsoccer_update_9050()` (set 35+ seasons to 35) |
| `config/sync/field.field.user.user.field_dob.yml` | `required: false` → `required: true` |
| `config/sync/core.entity_form_display.user.user.default.yml` | `contact` moved from content to hidden |

---

## Key Architectural Notes

**Registration flow architecture:** The `/register` page uses `RegistrationController` with direct links to `addSeasonToCart()` / `addTournamentToCart()` — NOT `RegistrationSelectForm`. Age checks must be in the controller methods, not just form validation. The `RegistrationSelectForm` validation serves as a secondary safeguard only.

**DOB widget quirk:** The Drupal datetime widget returns an array `['date' => '2024-01-15', 'time' => '']` instead of a string. The `_ccsoccer_parse_dob_value()` helper handles this, plus DrupalDateTime objects and plain strings.

**Age calculation reference date:** Age is calculated relative to the season/tournament `start_date` (standard sports league practice), not the current date. This is used in both the controller and form validation.

---

## For Other Developer

After pulling:
```
ddev drush updb -y   # Run update hooks 9049 + 9050 (minimum_age field + 35+ seasons)
ddev drush cim -y    # Import config (DOB required, contact hidden)
ddev drush cr        # Clear cache
```

**Testing checklist:**
- Create new account: First Name, Last Name, DOB, Gender should be required
- Try underage DOB (under 18): should be rejected
- Try username with special chars (not @): should be rejected
- Try registering for Men's 35+ as a player born after ~1991: should be blocked at cart add
- Change notification pref to "text" without phone: should require phone
- Enter phone with < 10 digits: should reject
- New user one-time login → set password → should redirect to profile page

---

## TODOs (Carried Forward)

### Next Session Priority

**Age Override System for Men's 35+:**
Add `override_type` field to the Override entity with an "age_exception" value. Add a method to `OverrideManagerService` to check for age overrides. Integrate into `RegistrationController::checkUserAge()` so players with an admin-granted age override can register for 35+ seasons even if underage. Add admin form for granting overrides. Consider post-checkout marking of override-used registrations.

**Player Picture Validation:**
Implement validation to ensure uploaded player photos contain a human face. Approach TBD — discuss with Andrew. Related: the duplicate picture field issue (`field_player_picture` vs `user_picture`) still needs resolution.

### Lower Priority

**Further UI Polish:**
- Home page content/hero area styling
- Content page typography and spacing
- Form styling improvements
- Consider whether top-level nav links should be pulled out of dropdowns on desktop

---

## Previous Session Archive

Previous handoff archived to: `archive/SESSION_HANDOFF_2026_02_16_branding.md`

---

**Session Status:** COMPLETE — Ready to commit
