# CC Soccer D11 - Session Handoff
**Date:** June 4, 2026
**Branch:** `main`

---

## Current State

### 🎉 SITE IS LIVE at ccsoccer.com
Soft-launch mode (IP whitelist active). Board members + beta testers testing. Tournament registration open for SLO Friendly 2026.

### Code — TEST and PROD in sync on main
All work from today's session committed and deployed to both TEST and PROD.

---

## Session Work — June 4, 2026

### Group Management — Leave Group ✅ COMPLETE

Players who accepted a group invitation can now leave the group themselves (when groups are not locked).

- New route `ccsoccer.group.leave` at `/my-group/{registration}/leave` (POST)
- New `leaveGroup()` method in `GroupController.php` — verifies non-manager, checks groups_locked, clears `group_id`/`invited_by`/`invitation_status`, marks the original accepted invitation as `declined` so the manager can re-invite
- `ccsoccer-group-manage.html.twig` — Actions column shows **Leave Group** button for non-manager season members when not locked
- `groups_locked` theme variable added to `ccsoccer_group_manage` in `ccsoccer.module`

### Group Management — Groups Locked Enforcement ✅ COMPLETE

`groups_locked` is now enforced server-side everywhere, not just in the template.

**Server-side blocks:**
- `invite()` — blocked if season groups locked
- `removeMember()` — blocked if season groups locked
- `acceptSeasonInvitation()` — blocked if season groups locked
- `leaveGroup()` — blocked if season groups locked

**Template changes:**
- `ccsoccer-group-manage.html.twig` — when locked: hides invite form, pending invitations table, and Remove/Leave buttons; shows 🔒 locked notice
- `ccsoccer-my-registrations.html.twig` — when locked: replaces Accept/Decline buttons with 🔒 locked message
- `myRegistrations()` controller passes `groups_locked` per invitation row

### Group Management — Figure Out Group Later Bug Fix ✅ COMPLETE

Choosing **Figure out group later** during checkout was incorrectly declining all pending invitations via `declinePendingInvitationsForRegistration()` in `OrderCompleteSubscriber.php`. Fix: only call that method when `$invitation_status === 'accepted'`. Invitations now stay pending and appear correctly on My Registrations after checkout.

### Admin — Dissolve Group ✅ COMPLETE

Admins can now dissolve a season group (e.g. a solo manager who is blocking other managers from inviting him).

- **Dissolve Group** button added to `GroupInvitationsForm.php` (season only)
- Two-step confirm: shows yellow warning with player/invitation counts before acting
- On confirm: clears all group membership fields from every registration in the group, declines all pending invitations, redirects to Roster Builder
- Cancel returns to normal form without changes

### Files Changed — June 4, 2026
- `web/modules/custom/ccsoccer/src/Controller/GroupController.php`
- `web/modules/custom/ccsoccer/src/EventSubscriber/OrderCompleteSubscriber.php`
- `web/modules/custom/ccsoccer/src/Form/GroupInvitationsForm.php`
- `web/modules/custom/ccsoccer/ccsoccer.module`
- `web/modules/custom/ccsoccer/ccsoccer.routing.yml`
- `web/modules/custom/ccsoccer/templates/ccsoccer-group-manage.html.twig`
- `web/modules/custom/ccsoccer/templates/ccsoccer-my-registrations.html.twig`

---

## Session Work — June 3, 2026

### Tournament Registration — wording and disclaimer updates ✅ COMPLETE
Board feedback addressed. Changes in `TournamentTeamPane.php`:

- **Option 1 (create):** "Create a new team (become captain)" — unchanged, disclaimer does the deposit job
- **Option 3 (none):** "I don't see my team / I'm a free agent" — clarifies both use cases
- **Option 4 (ccsoccer_pool):** "Play on a CCSoccer Team" — cleaner label
- **Deposit disclaimer checkbox:** Full substantive text replacing the old one-liner. Now reads: "I understand that a $[X] deposit is required to form a team. This deposit will be returned at the conclusion of the tournament, at the discretion of CC Soccer, provided my team competes in a spirit of fair play. Behavior including but not limited to fighting, excessive arguing with referees, or receiving red cards may result in full or partial forfeiture of the deposit."
- **CSS:** Added `.form-item-ccsoccer-tournament-team-deposit-acknowledgment .form-item__label { display: inline; font-weight: normal; }` to both CSS files (cosmetic only — checkbox label inline alignment). Correct class confirmed from page source; previous attempt used wrong class name.

### Tournament Players — Cancel Registration ✅ COMPLETE
New admin action to cancel a tournament registration directly from the players list. Analogous to the season cancel but simpler (no credits, no proration).

- **New form** `TournamentCancelRegistrationForm` at `src/Form/TournamentCancelRegistrationForm.php`. Extends `ConfirmFormBase`. Shows player name + tournament name, captain warning if applicable. Refund options: No refund, or Commerce refund via payment gateway (amount input, server-side validated — `#min` was removed from the hidden field to avoid browser HTML5 validation blocking submission on hidden fields).
- **Key debug finding:** Browser HTML5 validation silently blocks form submission when a hidden `<input type="number">` has `min="0.01"` but value is `0`. Removing `#min`/`#max` from the field and handling validation server-side in `validateForm()` was the fix.
- **Captain handling:** On cancel, clears the `captain` field on the team entity but does NOT delete the team — admin cleans up manually.
- **Route:** `ccsoccer.tournament_cancel_registration` at `/admin/ccsoccer/registration/{registration}/tournament-cancel`. Deliberately matches the simple pattern of the season cancel route (no entity-typed `{tournament}` in path — entity upcasting in path parameters interferes with `ConfirmFormBase` POST handling).
- **Actions column** added to `TournamentController::players()` table — Cancel button per row linking to the new form.

### Module Updates ✅ COMPLETE — Drupal core 11.3.11, Commerce 3.3.6
- Drupal core 11.3.10 → 11.3.11 (includes symfony/polyfill-intl-idn CVE-2026-46644 fix)
- Commerce 3.3.5 → 3.3.6
- symfony/polyfill-intl-idn 1.37.0 → 1.38.1 (CVE-2026-46644 security fix)
- symfony/* patch updates (7.4.x → 7.4.13)
- twig/twig 3.26.0 → 3.27.1
- guzzlehttp/guzzle 7.10.3 → 7.10.6
- No `drush updb` needed — no schema changes in this core update.

### WebAuthn 2.0.0-rc7 → 2.1.0-beta1 — DEFERRED
Intentionally split from core update. Needs dedicated auth flow testing session. The blocking Composer advisory (symfony/polyfill-intl-idn) is now resolved by the core update, so WebAuthn update should be unblocked next session.

### Files Changed This Session
- `web/modules/custom/ccsoccer/src/Plugin/Commerce/CheckoutPane/TournamentTeamPane.php`
- `web/modules/custom/ccsoccer/src/Form/TournamentCancelRegistrationForm.php` (new)
- `web/modules/custom/ccsoccer/src/Controller/TournamentController.php`
- `web/modules/custom/ccsoccer/ccsoccer.routing.yml`
- `web/modules/custom/ccsoccer/css/ccsoccer-base.css`
- `web/themes/custom/ccsoccer_theme/css/base.css`
- `composer.json`
- `composer.lock`

### Deploy Steps Applied
- TEST and PROD: `ccsDeploy && ccsCr` (no schema changes)
- No `drush updb` or `drush cim` needed

---

## Captain Upgrade Path — Decision Recorded
Discussed the path for a participant who later wants to become a captain (registered without deposit, now wants to form a team). Decision: **cancel and re-register**. Player gets a refund on cancellation, re-registers choosing "Create a new team (become captain)". Self-serve upgrade was considered (Become Captain button, standalone deposit product) but complexity-to-frequency ratio doesn't justify it (2-3 cases per tournament historically). Manual admin-mediated path also considered but cancel + re-register is cleaner for the deposit report.

---

## Session Work — May 27, 2026 (later session)

### CCSoccer Pool — remove-flag admin action ✅ COMPLETE
- New route `ccsoccer.tournament_players_remove_pool`
- New `TournamentController::removePoolFlag()` method
- ✕ link in Pool badge cell on Tournament Players page

### CCSoccer Pool — badge color unified ✅ COMPLETE
- `.pool-badge` background changed to `var(--color-danger)` in `season-players.css`

### Board Contact Preferences — opened to board members in dual mode ✅ COMPLETE
- Route permission relaxed to `view reports`
- `BoardContactPreferencesForm` dual-mode: admins see all, board members see only their own row
- New Quick Link tile added to board dashboard

### Help Center — two new sections ✅ COMPLETE
- CCSoccer Pool section in tournament teams content
- Board Contact Preferences section in board dashboard content

---

## Session Work — May 27, 2026

### Captain real name on /tournament-teams ✅ COMPLETE
### Show Unassigned Players + Show CCSoccer Pool on /tournament-teams ✅ COMPLETE
### Captain assignment on admin-created teams ✅ COMPLETE
### Help Center — captains documentation ✅ COMPLETE

*(See archived SESSION_2026-05-27.md for full detail on all of the above)*

---

## Session Work — May 25, 2026

### Tournament Team Name Uniqueness ✅ COMPLETE
### Sync Team Names from League — New Admin Action ✅ COMPLETE
### Season Page Button Cleanup ✅ COMPLETE
### CreditsPane Cleanup ✅ COMPLETE

*(See archived SESSION_2026-05-27.md for full detail)*

---

## Season Flag Enforcement Audit — June 4, 2026

Audit of the five season status checkboxes to verify they are enforced as intended.

### ✅ Registration Visible — Correctly Implemented
`/register` page filters with `registration_visible = TRUE`. Unchecking hides the season from the public registration page.

**Minor gap:** `active` is not checked alongside `registration_visible` on `/register`. A season with `active = FALSE` but `registration_visible = TRUE` would still appear for registration. Unlikely in practice since both are usually toggled together.

### ✅ Roster Visible — Correctly Implemented
Public Teams page and individual roster pages in `ContentController` gate on `roster_visible = TRUE`. Schedule Builder auto-sets it TRUE on publish, FALSE on clear.

**Minor gap:** Direct team URLs (e.g. `/teams/123`) may bypass the flag check at the individual page level — not verified at the entity route layer.

### ✅ Schedule Visible — Correctly Implemented
Public Schedule page in `ContentController` gates on `schedule_visible = TRUE`. Schedule Builder auto-sets it on publish/clear.

**Same minor gap as above** — direct game/schedule URLs may not recheck the flag.

### ✅ Groups Locked — Now Correctly Implemented
Was previously a UI hint only. Fixed this session — now enforced server-side in `invite()`, `removeMember()`, `acceptSeasonInvitation()`, and `leaveGroup()`. Templates also hide all group modification UI when locked.

### ⚠️ Active — Partially Implemented

`active` controls admin list visibility only. It does NOT gate registration or checkout.

| Context | Checked? |
|---|---|
| Admin season lists and dropdowns | ✅ |
| Public Teams / Roster / Schedule pages | ✅ |
| `/register` page (season appears for registration) | ❌ |
| Checkout / `OrderCompleteSubscriber` | ❌ |
| My Registrations cards for players | ❌ |

**Practical risk:** If you uncheck `Active` to retire a season, players can still register for it as long as `Registration Visible` remains checked. If someone has the product in their cart when `registration_visible` is toggled off, checkout will still complete (mid-checkout race condition).

### ✅ Gaps Resolved — June 4, 2026 (morning session)

**1 & 2 — `active` and `registration_visible` now enforced at checkout**

Added server-side guards in `RegistrationController` and `OrderCompleteSubscriber`:

- `addSeasonToCart()` — blocks cart entry if `registration_visible=FALSE` or `active=FALSE`; user sees error message and is redirected to `/register`
- `addTournamentToCart()` — blocks cart entry if `registration_visible=FALSE` or `status` is `completed`/`cancelled`
- `OrderCompleteSubscriber::createSeasonRegistration()` — defense-in-depth guard after season reload; blocks registration and logs for admin review if flags were toggled mid-checkout (race condition)

**3 — Direct entity URLs for roster/schedule — already implemented, no gap**

`ContentController` already has the admin override pattern on both the Teams and Schedule pages. Users with `generate schedules` permission can pass `?season=<id>` for any season regardless of `roster_visible` / `schedule_visible` / `active`, and see a yellow admin notice banner. Normal players and unauthenticated users cannot trigger the override and see the default season instead. This was implemented in commit `7a26733`.

**4 — Resolved by 1 & 2 above.** Flags are now hard gates at checkout, not just display hints.

---

## Combined Pre-Launch Checklist

### 🚨 Immediate

1. ~~**Caleb** — Audit what `drush cim` overwrites on PROD.~~ ✅ DONE

2. **Caleb** — Team names refactor (Phase 1 complete)
   - 2a. ✅ DONE
   - 2b. `/admin/ccsoccer/team/add` pre-filter by series — still open
   - 2c. ✅ DONE
   - 2d. Phase 3: Roster builder verification
   - 2e. Add league/series columns and filter to team names taxonomy overview
   - 2f. ✅ DONE
   - 2g. Admin "team-name collision review" view — still open

---

### Before Opening Whitelist to Public

3. **Andrew** — Vanishing CAPTCHA: confirm stays visible after correct CAPTCHA + wrong password.

4. ✅ DONE **Andrew** — Create real SLO Friendly 2026 season + tournament.

5. **Caleb** — Commerce checkout end-to-end with real card on PROD (season registration + jersey-only paths).

6. **Geo-blocking** — Site is now public and receiving Russian spam bot traffic via the contact form.
   - 6a. **Replicate D7 modules (preferred first step)** — D7 used `ip2country` + `ip_ban` for country-level blocking at the Drupal application layer. D11 equivalents exist and are compatible:
     - **Smart IP** (`smart_ip`) — geolocation lookup, replaces `ip2country`
     - **Country Block** (`country_block`) — blocks by country code, replaces `ip_ban`. Config at `/admin/config/system/country-block`, enter ISO codes (e.g. `RU`). Depends on Smart IP.
     - Smart IP needs an IP2Location LITE database file (free download from ip2location.com). Upload BIN file to server, point Smart IP config at it.
     - Neither module opens new attack surface. Country Block is not covered by Drupal security advisory policy but is low risk for this use case.
     - **Try this before Cloudflare** — no DNS changes, no traffic rerouting, self-contained.
   - 6b. **Cloudflare geo-blocking (fallback)** — Andrew's original item. Requires setting up a Cloudflare account and rerouting DNS through Cloudflare proxy (was painful last time). WAF custom rules can block by country before traffic hits the server. Only pursue if Smart IP / Country Block proves insufficient.

7. **Caleb** — Remove IP whitelist from `web/.htaccess` on go-live day (both servers, skip-worktree protected).

8. **Andrew** — Done - Delete `ccsoccer-d11-migrated-200users.sql` from repo root if present.

9. ✅ DONE — Pre-launch security checklist.

10. ~~**Caleb** — Security Metrics automated testing setup.~~ — IPs whitelisted; waiting on scan results.

11. ✅ DONE — PROD cron running every 15 min.

---

### Mid-term

12. `test@ccsoccer.com` mailbox decision.

13. Andrew's local environment DB update.

14. `slofriendly` config role reference cleanup (Andrew).

15. **Caleb** — WebAuthn passkey 2.0.0-rc7 → 2.1.0-beta1. Composer advisory now resolved — do next session with full auth flow test.

16. Backup strategy verification.

17. CSP headers in report-only mode.

---

### Post-Launch / First 72 Hours

18. ✅ DONE — Assign `permanent_override` role.

19. ✅ DONE — Verify credit balances against D7.

20. ✅ DONE — Check credits/registrations after April 16 dump date.

21. Remove `beta_tester` role from all users.

22. `slofriendlysoccer.com` URL forward.

23. Archive/delete D7 waivers; eventually delete `ccsoccer_site_d7_archive/`.

24. Monitor login rate, password resets, unhandled exceptions for first 72 hours.

25. Add Devel back to TEST.

---

### Development (Deferred)

26. Three-tier button methodology pass.

27. CSS consolidation — design tokens across ~33 CSS files.

28. Team handling refactor.

29. Profile picture migration — board decision pending.

30. Fix remaining `CC Soccer` → `CCSoccer` in tournament deposit + jersey notification subjects/SMS bodies.

---

## DB Quick Reference

### Production DB (live)
- DB: `n6ac4b5_d11live`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### TEST DB
- DB: `n6ac4b5_d11test`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### D7 archive DB
- DB: `n6ac4b5_ccsoccer`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### Local D11 DB
- Admin: `admin` / `TJ4XxyYGCd`

---

## Server Directory Structure
```
/home/n6ac4b5/public_html/
  ccsoccer_site/            ← PRODUCTION (ccsoccer.com)
  ccsoccer_site_d7_archive/ ← D7 archive (old.ccsoccer.com, PHP 7.2)
  test_ccsoccer_site/       ← TEST (test.ccsoccer.com)
  slofriendly_redirect/     ← slofriendlysoccer.com redirect
```

---

## Email Architecture

### Pipeline
```
NotificationService / OrderCompleteSubscriber
  → Symfony Mailer pipeline
    → Inline CSS + Theme wrapper (email.html.twig)
    → URL to absolute + Wrap and convert
  → Google Workspace SMTP (TEST + prod)
  → Mailpit (local DDEV only)
```

### Key Files
- `web/modules/custom/ccsoccer/src/Service/NotificationService.php`
- `web/modules/custom/ccsoccer/src/Plugin/QueueWorker/NotificationQueueWorker.php`
- `web/modules/custom/ccsoccer/src/Commands/NotificationCommands.php`
- `web/themes/custom/ccsoccer_theme/templates/email/email.html.twig`

### Notification gating
- `site_instance = 'local'` → board members only (Mailpit catches all)
- `site_instance = 'test'` → board members + beta_testers only (real emails)
- `site_instance = 'production'` → all users

### Queue Worker — SMS suppression
When `sms_body === ''` (explicitly empty), `processItem()` calls `sendEmail()` directly, bypassing `send()` which would strip HTML for SMS. This is how migration welcome email avoids SMS.

### SMTP config (in settings.local.php, never in git)
- Host: `smtp.gmail.com`, Port: `465`, TLS: true
- User: `ccsoccer@ccsoccer.com`
- App password: `sqygkfykzwrziota`

### reCAPTCHA Keys
- Site key: `6LchguosAAAAAC5kLFmKj0xCGdEWXNntLacANpVN`
- Secret key: `6LchguosAAAAANBJ8ikcrZvYQMxs6-FM2YHbzPEl`

### Authorize.net Live Credentials (PROD only — never commit)
- API Login ID: `9Fus5B2a`
- Transaction Key: `7MA8r27R9KQrs3Kd`
- Public Client Key: `5ep4C2xSvyY4jpra4694guKsJyV6XGq2CB39SHRtrs59wHH47avwTfKM7R7xJ7hF`
- Mode: Live
- Plugin: Authorize.net Accept.js
- Configure at: `/admin/commerce/config/payment-gateways`

**Note:** `drush cim` will overwrite these if payment gateway config is in `config/sync`. Always re-enter after any config import on PROD.

---

## Key Facts / Gotchas

### beta_tester role — do not delete from config/sync
Always `git checkout config/sync/user.role.beta_tester.yml` after `drush cex`.

### Google SMTP App Password
`sqygkfykzwrziota` — in `settings.local.php` on TEST and prod servers only.

### Symfony Mailer — User override
Do NOT enable the "User" override — replaces HTML with plain text.

### DB import fix for InMotion
```bash
gunzip -c dump.sql.gz | sed 's/DEFINER=[^*]*\*/\*/' | gzip > dump-clean.sql.gz
```

### Two-file CSS sync
- `web/modules/custom/ccsoccer/css/ccsoccer-base.css` (admin)
- `web/themes/custom/ccsoccer_theme/css/base.css` (public)

### composer install on servers
```bash
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer install --ignore-platform-req=ext-intl
```

### Migration welcome email — password reset URL
Hardcoded to `https://ccsoccer.com/user/password` in `sendMigrationWelcome()`. `Url::fromRoute()` generates `http://default/` in Drush CLI context.

### CAPTCHA admin path
`/admin/config/people/captcha` — must be logged in as `admin` user. Use private window to test.

### TournamentCancelRegistrationForm — hidden number field gotcha
Browser HTML5 validation silently blocks `ConfirmFormBase` submission when a hidden `<input type="number">` has `min` set but the value is below it. Remove `#min`/`#max` from hidden/conditionally-visible number fields and validate server-side in `validateForm()` instead.

### ConfirmFormBase + entity-typed route parameters
Do NOT put entity-typed parameters (e.g. `type: entity:tournament`) in the route path for a `ConfirmFormBase` form. Entity upcasting interferes with the form POST pipeline and causes silent submit failures. Keep the path simple (raw integer parameter only) and derive entity context from the registration itself in `buildForm()`.

---

## Server Quick Reference
```bash
# SSH in
ssh ccsoccer

# TEST deploy (includes composer install)
ccsDeploy && ccsCr

# PROD deploy (includes composer install)
ccsProdDeploy && ccsProdCr

# Full deploy with DB updates
ccsDeploy && ccsUpdb && ccsCim && ccsCr
ccsProdDeploy && ccsProdUpdb && ccsProdCim && ccsProdCr

# Drush full path (TEST/PROD)
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web [command]
```

## .htaccess — both servers
Not in git — protected via `git update-index --skip-worktree web/.htaccess`

## Git Workflow
- Always `git pull` before `git push`
- `main` is the primary branch
- `settings.local.php` is NOT in git
- Always `git checkout config/sync/user.role.beta_tester.yml` after `drush cex`
