# CC Soccer D11 - Session Handoff
**Date:** July 8, 2026
**Branch:** `feature/update_registration_page` — 2 commits, tested on LOCAL, needs PR + deploy

Earlier session detail (June 23 – July 5) archived in `archive/SESSION_HANDOFF_2026_07_08.md`.

---

## Session Work — July 8, 2026 (later session) — branch `feature/update_registration_page`

### 1. Registration page shows already-registered items (commit 3330566)

Problem: `/register` was empty for fully-registered players (bad UX — they had
to know to go to `/my-registrations`). Root cause: `RegistrationController::available()`
already split visible seasons/tournaments into registered vs available arrays,
but the registered arrays were **built and never rendered**.

- Registered cards now render in the same Leagues/Tournaments sections, after
  available ones: muted card (`season-card--registered`), green "✓ You Are
  Registered" box, **outline** Manage Group / Manage Team button (My
  Registrations style). Solid red buttons now mean only "Register".
- Tournament registered button renamed "Review Team" → "Manage Team" to match
  My Registrations.
- **Filter pills now honored by registered cards too** (second iteration —
  first pass showed registered items on every filter). Filter check moved
  ahead of the registered/available split in both loops. Coed shows only coed
  (available + registered), Men's only mens, Tournaments only tournaments,
  All shows everything. Side benefit: filtered-out items skip per-user
  registration queries.
- Added `url.query_args:filter` cache context (page previously varied only by
  user — cross-filter cache bleed risk under dynamic page cache).
- Empty-state check simplified — all four arrays honor the filter now.
- Home page buttons: dropped "Open" → "See Coed Registrations", "See Men's
  Registrations", "See Tournament Registrations" (`ContentController.php`).
  Rationale: page now also shows registered items, and players registered for
  the current season (summer) must still find the next season (fall) there.
  `CONTENT_PAGES_CSS_CHECKLIST.md:85` updated to match.
- Files: `RegistrationController.php`, `registration.css`,
  `ContentController.php`, `CONTENT_PAGES_CSS_CHECKLIST.md`.

### 2. Tournament teams: captain badged inline (commit 44d337b)

Captain appeared twice on `/tournament-teams` cards ("Captain:" line + roster).
Now: no "Captain:" line; captain sorts first with red CAPTAIN pill, co-captain
second with gray CO-CAPTAIN pill, rest alphabetical. Current-user highlight
unchanged. Fallback: "Captain:"/"Co-Captain:" line still renders **only** when
that person is not in the players list (admin-created teams, unregistered
captains). Files: `ContentController::tournamentTeamsPage()`,
`content-pages.css` (pill styles matching My Registrations badge).

### Deploy — code only, no cim/updb; run in EACH environment
```bash
drush cr
```
Verify after deploy: `/register` as a fully-registered player (cards, not
empty), filter pills show only their category, `/home` button labels,
`/tournament-teams` captain pills + fallback line on captain-only teams.

---

## Previous Session Work — July 8, 2026 (earlier) — ✅ MERGED via PR #115

### Season + tournament dates on player-facing pages

Players could not see season/tournament dates anywhere on `/register` or
`/my-registrations` — dates were admin-only. Root cause on the tournament card:
`buildTournamentCard()` checked a **nonexistent `tournament_date` field** (dead
code — entity uses `start_date`/`end_date`), which is why the SLO Friendly card
showed no date. Season cards never read dates at all.

#### What changed (6 files)
- **`src/DateRangeFormatTrait.php`** (new) — shared formatter. Single day →
  "Saturday, September 5, 2026"; same-year range → "Aug 20 – Oct 15, 2026";
  cross-year range → years on both ends. Falls back when only one date set;
  returns '' when neither. (`formatDateRange()` remains duplicated in
  SeasonListBuilder/TournamentListBuilder — refactor to the trait is optional
  cleanup later.)
- **`src/Controller/RegistrationController.php`** — season cards add
  "Season Dates: …" under the League line; tournament cards use
  start_date/end_date ("Date: Saturday, September 5, 2026"); pending
  invitations banner shows dates.
- **`src/Controller/GroupController.php`** — `myRegistrations()` adds a
  preformatted `dates` string to season, tournament, and pending-invitation
  rows (entities already loaded, no extra queries).
- **`templates/ccsoccer-my-registrations.html.twig`** — muted date line under
  the title on season cards, tournament cards, and invitation cards.
- **`css/registration.css`** — `.registration-card__dates`,
  `.invitation-card__dates`, `.invitation-item__dates` (small, muted).

#### Test on LOCAL (drush cr first — new class + template + CSS cache-bust)
1. `/register` logged out and logged in — dates on both league cards and the
   tournament card.
2. `/my-registrations` — dates under Mens/Coed/SLO Friendly cards.
3. Pending invitation cards on both pages show dates.

#### Deploy — run in EACH environment (LOCAL → TEST → PROD)
Code only; no `cim`, no `updb`.
```bash
drush cr
```

#### Unrelated note
`web/sites/default/default.settings.php` scaffold drift (from Caleb's core
bump — see June 26 archive note) is still uncommitted. Keep it OUT of the
date-display commit; commit separately with the core upgrade.

---

## Anonymous checkout lockdown — step 2 DONE on LOCAL; steps 1, 3, 4 still open

Full write-up: `ANONYMOUS_CHECKOUT_LOCKDOWN.md`. Reviewed against Caleb's
email July 5; the doc's 4-step plan stands. His route-subscriber default-deny +
anonymous route sweep is good follow-on hardening (slot next to checklist item
17/CSP), not a launch blocker.

### Background (Wayne / order 61)
Wayne (Bakersfield captain) bought "Team Fee — SLO Friendly 2026" while NOT
logged in — order 61, no user account tied to it. Root cause in `config/sync`:
the checkout flow's `login` pane is `_disabled`, and the anonymous role had
`access checkout` + `view commerce_product`. Season/tournament/jersey are
protected only per-product, not by a global login gate — so any product
exposing a standard add-to-cart form was anonymously buyable (Team Fee, any
`default`-type product, leftover `security_metrics_test_product_ty`). Risk:
orphaned orders and **card testing against the live Authorize.net gateway**.
Invite flow NOT affected (invitees authenticate before checkout). Caleb
manually reconciled Wayne's team + disabled the product; next year's team-fee
flow should require login (checklist item 34).

### Plan status
1. **OPEN** — Re-enable `login` checkout pane, require login. NOT config-only:
   `CCSoccerCheckoutFlow::getSteps()` defines no `login` step, so flipping
   `step: _disabled` alone points the pane at a nonexistent step. Proper fix:
   add a `login` step first in `getSteps()` + pane config `step: login`,
   `allow_guest_checkout: false`. Decision open: `allow_registration`
   true/false (false probably fine). Test LOCAL → TEST before PROD.
2. **✅ DONE on LOCAL** — removed `access checkout` from anonymous
   (`config/sync/user.role.anonymous.yml`, `commerce_checkout` dependency
   removed). Validated: logged-out `/checkout` is 403; logged-in checkout and
   invite flow unaffected. This alone closes the carding hole.
   Deploy to TEST then PROD: `drush cim -y` (no cr, no updb), then verify
   logged-out `/checkout` 403 + a logged-in checkout completes.
3. **OPEN** — Remove `security_metrics_test_product_ty` test product type/product.
4. **OPEN** — Keep Team Fee unpublished; next year's flow login-required (item 34).

### Team pages PII check (Caleb's question) — ✅ CLEAR
No member phone/email/names reach anonymous visitors on any team surface;
iCal feed token-validated; anonymous lacks `access user profiles`. Minor nit
(no action): `/teams` gate lives in the controller, not the route.

### Discovered: media display config drift loop (pre-existing)
`core.entity_view_display.media.image.default`, `…media.image.media_library`,
`…media.document.media_library` show `Different` immediately after `cim` —
media_library re-tweaks its own displays after save. Likely fix: `cex` just
those three files (remember beta_tester gotcha). Flag to Caleb.

---

## Open security follow-ups (from July 3 review)

Details in `CC_Soccer_Security_Review_2026_07_03.md` + archive.

- **E (MEDIUM)** — `JerseySelectionPane::submitPaneForm` trusts submitted
  `variation_id`; verify it is a jersey (SKU `JERSEY-%`) before adding to cart.
- **I (LOW)** — Scrub + rotate Authorize.net **test** creds and reCAPTCHA
  **secret** from git.
- **K (LOW)** — Generic exception messages (stop returning `$e->getMessage()`),
  move inline `onchange` in `ReportController.php:393`, escape admin notes on
  any future display.
- **G residual** — gate `invitee_email` fee waiver on the `invitee` reference
  before team fees scale next year; docblock cleanup
  (`GroupInvitationsForm.php:24`, `Team.php:655-657`).
- **Flagged-orders admin report** — surface `ccsoccer_credit_shortfall`,
  `ccsoccer_season_registration_failed`, `ccsoccer_team_name_collision`,
  `ccsoccer_tournament_full`, `ccsoccer_team_capacity_exceeded` (checklist
  item 18; design in `FLAGGED_ORDERS_REPORT_PROPOSAL.md`). Interim: filter
  `/admin/reports/dblog` by `ccsoccer` channel.
- **#3 (watch)** — `filter_htmlcorrector` removed from full_html; unclosed tags
  in pasted rules HTML could break page DOM. Fix when rules re-entered natively
  next year.

---

## 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 — main has PR #115 (date display) merged; `feature/update_registration_page` (2 commits) awaiting PR. TEST/PROD deploy status of both: verify.

---

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