# CC Soccer D11 - Session Handoff
**Date:** June 24, 2026
**Branch:** `main` — all environments in sync

---

## Session Work — June 26, 2026 (later)

### Consistent player search across all surfaces — ⚠️ READY TO PUSH (branch `fix/search`)

Made every place an admin or player searches for another player behave the same way: split a multi-word query on spaces and AND the terms together, so each term must match at least one field (username, first name, last name, or email). This fixes first+last name searches that previously returned nothing because the whole string was matched against each field individually and first/last names live in separate fields.

#### Root cause
The improved invite search (commit `cd01fd7`) already split terms, but the other search surfaces still matched the full string (`%Nathan Perry%`) against each field, which can never match a record whose first name is `Nathan` and last name is `Perry`. So first-only and last-only worked, but first+last together returned 0.

#### What changed (4 files)
- **`src/Plugin/EntityReferenceSelection/UserByNameSelection.php`** — the masquerade footer autocomplete. Splits terms across `name` / `field_first_name` / `field_last_name`; falls back to default behavior on empty/whitespace input.
- **`src/Controller/PlayerAdminController.php`** — the All Players admin page (`/admin/ccsoccer/players`). Splits terms in both the result query and the count query.
- **`src/RegistrationListBuilder.php`** — `findUserIdsByName()` used by the registration list. Splits terms across name/mail/first/last.
- **`js/season-players.js`** — the shared client-side filter for the Season **and** Tournament player pages. Splits the search term so any word order matches (e.g. `Perry Nathan`), and team-name search keeps working. (The tournament page already worked for full names because `data-name` holds `First Last`; this makes reversed order consistent too.)

The player **invite** search (`GroupController::userSearch`) was already correct and was left untouched.

#### Deploy note (same stale-asset gotcha as below)
`season-players.js` is served with a cache-busting query string that only regenerates on cache rebuild. After deploying, run `drush cr` in each environment or the old JS keeps being served — same root cause as the tournament team-search finding documented in the earlier June 26 session below.

#### Unrelated note — `default.settings.php`
After pulling Caleb's `composer.lock` core bump (11.3.11 → 11.3.13) and running `composer install`, `drupal/core-composer-scaffold` re-copied `web/sites/default/default.settings.php` from the new core template. The only change is an added commented-out doc block for `media_oembed_discovery_trusted_host_patterns` — all comments, no active code, and it is the template (not the live `settings.php`). Should be committed **separately** from the search work (belongs with the core upgrade), not folded into the `fix/search` commit.

---

## Session Work — June 26, 2026

### Hotfix: Tournament `group_id` mismatch from checkout registrations — ⚠️ READY TO PUSH

A hotfix (not a feature). Fixes the disconnect where players who joined a tournament team **through checkout** showed up on the team roster but not in the captain's group — invisible in the Roster Builder yellow group and uncounted on the Group Invitations admin page, even though Manage Team and Tournament Teams views showed them.

#### Root cause
The Team entity `group_id` is a **UUID** and is the single source of truth for group membership (Roster Builder yellow grouping, Group Invitations admin page, `GroupController` / `RegistrationController` join flows). `update_9036` migrated existing data from the old deterministic format `team_<teamID>_<captainID>` to that UUID — but `OrderCompleteSubscriber` was never updated and kept writing the legacy string for every new tournament registration (create / token_accept / join branches). So checkout-registered players got a `group_id` that never matched their team UUID. Every place that compares `group_id` dropped them from the group; views that read the team `players` list still showed them, which is why it looked inconsistent.

#### What changed
- **`src/EventSubscriber/OrderCompleteSubscriber.php`** — all three join branches now route through a new `getOrCreateTeamGroupId($team)` helper that returns the team's UUID `group_id` (generating + saving one only if absent), matching the rest of the system. Legacy `team_<id>_<id>` string writes removed.
- **`ccsoccer.install`** — added idempotent update hook `ccsoccer_update_9068()` + helper `_ccsoccer_repair_legacy_team_group_ids()`. Repoints existing registrations whose `group_id` starts with `team_` to their own team's UUID and sets `invitation_status = accepted`. Leaves NULL `group_id`s untouched to preserve genuine free agents. Called from `hook_install()` per module convention. Safe to re-run (no-op after first pass).

#### Unrelated second finding (NOT a code fix)
Team search on the Tournament Players page works locally but returned 0 on PROD. This is a stale-asset issue, not a data bug: the team-aware search (`team.includes(searchTerm)` in `js/season-players.js`) shipped in commit `931b193`. PROD was serving the older aggregated JS. Resolved by `drush cr` after confirming `931b193` is deployed.

#### Deploy steps — run in EACH environment (LOCAL → TEST → PROD)
Deploy code first, then:
```bash
drush updb -y      # runs ccsoccer_update_9068 (data repair); logs repaired count
drush cr           # clears caches; rebuilds aggregated JS → fixes team search
```
Local DDEV: `ddev drush updb -y && ddev drush cr`. `updb` tracks per-environment so the repair applies exactly once each; re-running is safe.

#### Verification
- `drush updb` prints "Repaired N registration(s)…"; same count goes to watchdog/dblog.
- Roster Builder: previously-orphaned checkout players now appear in the captain's yellow group.
- Group Invitations admin page: those players now count as Accepted instead of being missing.

---

## Session Work — June 23-24, 2026

### Team-Paid Tournament Registration — ✅ COMPLETE, LIVE ON PROD

Full feature shipped to LOCAL → TEST → PROD. All three environments in sync on main.

#### What was built:
**Team Fee product type** (`team_fee`) — Commerce product type for whole-team tournament entry fees. Mirrors `tournament_registration` with `field_tournament` tie. Single variation per product, cart view display configured. Config in git; deployed via `cim`.

**`team_paid` flag on Team entity** — Boolean field, admin/TD-only (form-visible, view-hidden). Defaults FALSE. Set on Team edit form; not surfaced to captains this year. Next year the Team Fee product checkout will set it automatically. Deployed via `ccsoccer_update_9067`.

**TournamentTeamPane reshape** — Tight model: uninvited players see no join option. Invited players see per-team radio ("Join [team name]"), preselected when only one invite. Unified invite resolution: session token + invitee ref + exact email match. Session token validated against current user (security fix — stale tokens self-clear on masquerade switch). Dead code removed (getExistingTeams, attachDisabledTeams, tournament-team-select JS, elseif 'join' in processAction). Full teams omitted from invited options (radios have no per-option disabling). OrderCompleteSubscriber untouched — pane emits existing `token_accept` contract.

**TeamPaidOrderProcessor** — Line-item adjustment (not order-level) zeroes the `tournament_registration` line when `action = token_accept` AND `team.team_paid = TRUE`. Priority 200, runs before DiscountOrderProcessor (100). Dark deploy: no-op until an admin marks a team paid.

**DiscountOrderProcessor fix** — Now skips items already covered by a `team_paid_registration` adjustment. Prevents negative totals when a board member (100% discount) registers on a paid team. Discountable amount computed per-item so deposits/other items still get the board discount.

**Welcome email fix** (`user.mail.yml`) — Restored `[user:one-time-login-url]` to `register_no_approval_required` body. Also added `[site:login-url]` and clearer activation copy. The token had been dropped in commit 038a223 (May 19), leaving only a static `/register` link that contradicted the confirmation page.

#### To activate for Bakersfield / Wayne:
1. ~~Create "Team Fee — SLO Friendly 2026" product on PROD~~ ✅ DONE — [/products/team-fee-slo-friendly-2026](https://ccsoccer.com/products/team-fee-slo-friendly-2026)
2. Wayne adds to cart and pays.
3. Admin checks `team_paid` on their team at `/admin/ccsoccer/team/{id}/edit`.
4. Captain invites players by email.
5. Players register → price drops to $0 automatically.
6. Refund-not-cancel for anyone who already registered individually before flag was set.

#### Known deferred items from this feature:
- CSS: "Join [team name]" radio label not rendering bold/larger (`:has()` selector not firing or Safari cache). Cosmetic only.
- Email-match entitlement: `invitee_email` match grants waiver; Drupal doesn't re-verify email on change. Low risk at $35 this year; revisit when team fees scale next year.
- Completion-race: if a team-paid team fills between zeroed checkout and order completion, player gets free entry without being on the paid roster. Rare edge case, no fix planned yet.
- Next year: Team Fee checkout writes `team_paid` flag automatically; captain sees "team paid" badge on dashboard; `team_paid` + deposit swap at captain checkout; board approval needed for public team-fee path.

#### Branch cleanup (when feature has baked in PROD, no rush):
```bash
git push origin --delete team-paid-registration
git branch -d team-paid-registration
```

---

## Session Work — July 2, 2026

### Tournament Series Rules Page — IN PROGRESS

Adding a dedicated rules page for tournament series (e.g. SLO Friendly). Replaces the current pattern of hand-typing a rules PDF URL into the tournament description field each year.

#### What we're building:
- **`machine_name` field on `TournamentSeries`** — explicit slug (e.g. `slo-friendly`), set once at creation, not derived from name. Stable URL anchor even if the series name changes.
- **`rules` field on `TournamentSeries`** — `text_long` WYSIWYG (CKEditor), separate from `description`. Tournament director pastes/edits rules content directly in Drupal.
- **Dedicated route** `/tournament-series/{machine_name}/rules` — public, renders rules only. Clean URL, printable, bookmarkable. 404s if no series found or rules empty.
- **`manage tournament series rules` permission** — scoped permission so tournament directors can edit the rules field without getting full series edit access (pricing, schedule defaults, etc.).
- **Auto-link on home page tournament card** — `ContentController::homePage()` checks if the tournament has a series reference with rules populated and auto-appends a "View Rules" link. Replaces the hand-typed link in the tournament description field.
- **New Twig template** — `ccsoccer-tournament-series-rules.html.twig` for the rules page output.
- **Update hook `ccsoccer_update_9069`** — idempotent helper adds both new fields to the `tournament_series` table. Called from `hook_install()` per module convention.

#### Files touched:
1. `src/Entity/TournamentSeries.php` — added `machine_name` and `rules` fields
2. `ccsoccer.install` — Phase 31 helper + update hook 9069 + hook_install call
3. `ccsoccer.routing.yml` — new `ccsoccer.tournament_series_rules` route
4. `ccsoccer.permissions.yml` — new `manage tournament series rules` permission
5. `src/Controller/ContentController.php` — new `tournamentSeriesRules()` method + updated `homePage()` tournament card
6. `templates/ccsoccer-tournament-series-rules.html.twig` — new template

#### Deploy steps (after code lands):
```bash
drush updb -y    # runs ccsoccer_update_9069 — adds machine_name + rules fields
drush cr
```
Then on the SLO Friendly series record: set `machine_name` to `slo-friendly`, paste rules content into `rules` field, save.
The home page tournament card will auto-show the "View Rules" link once the field is populated.

---

## Current State

### 🎉 SITE IS LIVE at ccsoccer.com
Soft-launch mode. Board members + beta testers testing. Tournament registration open for SLO Friendly 2026. Team-paid feature live and dark (no-op until a team is flagged).

### Code — LOCAL, TEST, PROD all in sync on main

---

## 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)** — Smart IP + Country Block. Smart IP needs IP2Location LITE BIN file (free, monthly update). Try before Cloudflare.
   - 6b. **Cloudflare geo-blocking (fallback)** — requires DNS rerouting through Cloudflare proxy.

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. ✅ DONE — Security Metrics test product.

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

---

### Mid-term

12. ~~`test@ccsoccer.com` mailbox / from address.~~ ✅ DONE — June 20, 2026.

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.

18. Admin view/report to surface orders flagged `ccsoccer_team_name_collision`, `ccsoccer_team_capacity_exceeded`, `ccsoccer_tournament_full`.

19. Confirm InMotion server's transactional email passes SPF/DKIM under DMARC `p=quarantine`.

20. Module/core updates — ready to run next weekend once team-paid has baked in PROD.

---

### Post-Launch / First 72 Hours

21. ✅ DONE — Assign `permanent_override` role.

22. ✅ DONE — Verify credit balances against D7.

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

24. Remove `beta_tester` role from all users.

25. `slofriendlysoccer.com` URL forward.

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

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

28. Add Devel back to TEST.

---

### Development (Deferred)

29. Three-tier button methodology pass.

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

31. Team handling refactor.

32. Profile picture migration — board decision pending.

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

34. `#slofriendly` `#tournament-nextyear` — Team Fee product can be purchased anonymously (not logged in). Order 61: Wayne (Bakersfield captain) bought "Team Fee — SLO Friendly 2026" as Anonymous (not verified), no user account tied to the order. Not fixed now — Caleb manually reconciling Wayne's team + disabling the product. Next year's productized team-fee flow should require login (or capture/match purchaser identity) before allowing purchase.

---

## 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 (subdomain removed; files still on disk)
  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

### Local invite email blocking
Non-production environments block invite emails to non-allowlisted addresses by design
(watchdog: "Email blocked to @to (not in board allowlist)"). This is correct behavior —
emails are generated but not sent. To test invite email content on LOCAL, invite a
board-member email address; it will land in Mailpit.

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

### From address — per environment (via settings.local.php, NOT config_ignore)
- **LOCAL**: `local@ccsoccer.com` — caught by Mailpit, never delivered
- **TEST**: `test@ccsoccer.com` — Google Workspace alias, same inbox as prod
- **PROD**: `ccsoccer@ccsoccer.com` — canonical value in `system.site.yml`; no override

Override pattern: `$config['system.site']['mail'] = 'test@ccsoccer.com';` in `settings.local.php`.

### 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`
- Domain-locked to production. LOCAL intentionally disabled — protected via `config_ignore`.

### 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`

### Drupal Update Notification Settings (per-environment, protected via config_ignore)
- **TEST**: weekly, `noreply@example.invalid`, security updates only
- **PROD**: weekly, `ccsoccer@ccsoccer.com`, all newer versions

---

## Key Facts / Gotchas

### config_ignore — environment-specific config that must never sync
```yaml
ignored_config_entities:
  - 'commerce_payment.commerce_payment_gateway.*'
  - 'update.settings'
  - 'recaptcha.settings'
  - 'captcha.captcha_point.*'
```
**When adding entries:** hand-edit the file directly, `git add` by name only — never blanket `cex`.
**After changing:** run `drush cim` on LOCAL to import the updated ignore list into active config.

### config drift check
```bash
drush config:status
```
Run before any `cex`, after deploys, and before merging feature branches. Reports `Only in sync`
(not imported), `Only in active` (drift — not exported), and `Different` (mismatch). `Different`
is the dangerous one. Edit config YAML directly in `config/sync` rather than via UI to avoid drift.

### Role config drift on LOCAL — unresolved, watch for recurrence
June 16: `user.role.anonymous`, `user.role.board_member`, `user.role.tournament_director`
showed unexpected drift. Cause unknown. Reverted via `drush cim`.

### 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)

### Commerce cart view mode
The cart's "Item" column renders the variation in its `cart` view mode (not the order item title).
New product types need `core.entity_view_display.commerce_product_variation.{type}.cart.yml`
or the cart falls back to rendering fields (showing "Price" as the label).

### 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
```

### TournamentCancelRegistrationForm — hidden number field gotcha
Remove `#min`/`#max` from hidden number fields; validate server-side instead.

### ConfirmFormBase + entity-typed route parameters
Do NOT put entity-typed parameters in route paths for `ConfirmFormBase` forms — use raw integer only.

### DMARC aggregate reports
Daily from `noreply-dmarc-support@google.com` and `dmarcreport@microsoft.com`.
Gmail filter to skip inbox + label. Check after any outbound mail config change.

### team_paid flag
Set on Team entity edit form (admin/TD only). Not visible to captains. Dark deploy — no-op
until explicitly set. Next year: Team Fee product checkout sets it automatically.
TeamPaidOrderProcessor (priority 200) applies a LINE-ITEM adjustment (not order-level) so
getSubtotalPrice() reflects the zero — prevents double-discount with DiscountOrderProcessor (100).

---

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

# 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`
- When editing `config_ignore.settings.yml`, commit that file by name only
