# CC Soccer D11 - Session Handoff
**Date:** May 27, 2026
**Branch:** `main`

---

## Current State

### 🎉 SITE IS LIVE at ccsoccer.com
Soft-launch mode (IP whitelist active). Board members + beta testers testing.

### Code — TEST and PROD in sync; LOCAL has uncommitted work from May 25 and May 27
Local has uncommitted changes from two sessions (May 25 and May 27, see below). TEST and PROD still on the May 23 `main` head. No pending deploys until the local work is reviewed and committed. May 27 adds a new `unassigned_visible` + `ccsoccer_pool_visible` base field pair on the Tournament entity, so `ddev drush updb` will be required on each environment when this work is deployed.

---

## Session Work — May 27, 2026

All changes local-only — not yet committed.

### Captain real name on /tournament-teams ✅ COMPLETE
The public Tournament Teams page (`ContentController::tournamentTeamsPage` and `myTournamentTeamsPage`) was showing the captain's login name (e.g. `andrewccsoccer`) instead of their real name. Switched both spots from `User::getDisplayName()` to the existing `getPlayerName()` helper that uses `field_first_name` + `field_last_name` and falls back to the account name. Same logic already used for the player roster, so the captain now renders consistently with the names below.

### Show Unassigned Players + Show CCSoccer Pool on /tournament-teams ✅ COMPLETE
Two new boolean base fields on the Tournament entity to control whether the public Tournament Teams page shows extra cards for players who registered but are not yet on a team.

- Fields `unassigned_visible` (label "Show Unassigned Players") and `ccsoccer_pool_visible` (label "Show CCSoccer Pool") added to `Tournament.php` at weights 15.1 and 15.2 (after `roster_visible`). `Division` form weight bumped from 15 to 15.5 so the five visibility checkboxes group together on the edit form
- Both default to FALSE so existing tournaments are unchanged. Idempotent helper `_ccsoccer_ensure_tournament_roster_group_visibility_fields()` added to `ccsoccer.install` + wired into `hook_install()` (PHASE 28) and `ccsoccer_update_9066()`
- `TournamentForm::populateFromClone()` resets both flags to FALSE on clone
- `tournamentTeamsPage()` now appends a dashed-border card per enabled flag, after the teams loop. Cards show a player count in the header, an italicized subtitle, the current user highlighted (existing `.current-user` rule), sort alphabetically, and scroll internally (`max-height: 480px` ≈ 20 rows). Empty cards render with "None" placeholder so a player still sees the section exists. Query filters registrations to `status IN (paid, active)` with `team` NULL, then splits on `ccsoccer_pool`
- Empty-state check updated to also account for the extra cards
- CSS additions in `content-pages.css`: `.team-card.group-card`, `.unassigned-card` (gray dashed border), `.pool-card` (warning-tone dashed border), `.scrollable-roster`, `.roster-empty`
- Status Flags admin tiles on `/admin/ccsoccer/tournament/{id}` (`TournamentController::view`) updated to show both new flags between Roster Visible and Schedule Visible. Read-side uses `hasField()` guards so the page does not break if `updb` has not been run yet

### Captain assignment on admin-created teams ✅ COMPLETE
Teams created by a tournament director from the Roster Builder (`+ Create Team`) historically had no captain and no UI path to assign one. Researched the captain semantics first (Team.captain entity ref + Registration.is_captain bool, kept in sync by `GroupInvitationsForm::promoteToCaptain` / `releaseCaptain`; admin teams already get a `group_id` UUID but never get a captain). Concluded the cleanest fix was to expose the existing `/admin/ccsoccer/group/{uuid}/invitations` flow on captainless teams — the promote/release machinery already handles the no-current-captain case (`promoteToCaptain` line 842 guards the demote step with `if ($old_captain_reg)`).

Three changes:

- **UI doorway (`TournamentRosterBuilderForm`):** Exposed `Team.group_id` on the team data array in `loadTournamentData()`. `renderTeamPlayersGrouped()` takes a new optional `team_group_id` arg. When no captain group renders but the team has a `group_id`, a lightweight management container is built that holds only the three-dots link pointing at the same Group Invitations admin route. New `buildManagementOnlyContainer()` helper. New CSS class `.player-group.management-only` in `tournament-roster-builder.css` strips the gold group background so the standalone link sits unobtrusively at the team column's top-right
- **Drag handler bug (`TournamentRosterBuilderController::movePlayerToTeam`):** Dragging onto a captainless team only set `registration.team`, never `registration.group_id` or `invitation_status` (the drag handler intentionally treats dragged players as "free agents" — correct for captained teams). For captainless teams this meant the Group Invitations page found zero registrations. Fixed: when the destination team has no captain, the drag handler now also sets `group_id` to the team's `group_id` and `invitation_status` to `accepted`. Captained-team behavior preserved (still free-agent on team)
- **Self-healing backfill (`GroupInvitationsForm::buildForm`):** When `loadByProperties(['group_id' => ...])` returns empty but a `Team` entity carries that `group_id`, the form now backfills the team's player registrations (idempotent — only touches rows whose group_id differs), logs the sync, and re-queries. Teams that had pre-existing dragged players self-heal on first visit to the Group Invitations page. Empty-state error text updated to suggest the next step

### Help Center — captains documentation ✅ COMPLETE
Appended new section "Captains, Co-Captains, and the Three-Dots Menu" to `HelpCenterController::getTournamentTeamsContent()`. Four subsections: what a captain can do, how captains normally get set (checkout vs admin-created), what the three-dots menu opens and the five actions available there, and step-by-step workflow for assigning a captain to an admin-created team. Uses `&rsquo;` / `&ldquo;` HTML entities throughout to avoid breaking the single-quoted PHP heredoc.

### Files Changed This Session
- `web/modules/custom/ccsoccer/ccsoccer.install`
- `web/modules/custom/ccsoccer/css/content-pages.css`
- `web/modules/custom/ccsoccer/css/tournament-roster-builder.css`
- `web/modules/custom/ccsoccer/src/Controller/ContentController.php`
- `web/modules/custom/ccsoccer/src/Controller/HelpCenterController.php`
- `web/modules/custom/ccsoccer/src/Controller/TournamentController.php`
- `web/modules/custom/ccsoccer/src/Controller/TournamentRosterBuilderController.php`
- `web/modules/custom/ccsoccer/src/Entity/Tournament.php`
- `web/modules/custom/ccsoccer/src/Form/GroupInvitationsForm.php`
- `web/modules/custom/ccsoccer/src/Form/TournamentForm.php`
- `web/modules/custom/ccsoccer/src/Form/TournamentRosterBuilderForm.php`

### Deploy / Apply Steps
After pulling on any environment:
1. `ddev drush updb -y` (installs `unassigned_visible` and `ccsoccer_pool_visible` via `ccsoccer_update_9066`)
2. `ddev drush cr`

### Still Open / Not Done This Session
- **May 25 follow-ups all still open** — see May 25 section below (team-name collision review view, manual `/admin/ccsoccer/team/add` league filter, taxonomy overview columns, `createTeamNameTerm()` rename, destructive "Wipe and Rebuild" variant)
- **Option B (nicer captain-assignment UX)** — explicit "Assign Captain" button on captainless team headers in the Roster Builder, opening a player picker. Decided to ship Option A first and layer this on later if Andrew finds himself doing the workflow often enough that the extra discoverability is worth building. The current path requires clicking the three-dots → landing on Group Invitations → using the Status dropdown
- **Co-captain assignment from Roster Builder** — same dropdown on Group Invitations also supports co-captain promotion (only available when captain exists and no co-captain). Out of scope this session

---

## Session Work — May 25, 2026

All changes local-only — not yet committed, reviewed in Caleb's environment, or deployed.

### Tournament Team Name Uniqueness ✅ COMPLETE
Andrew identified that two captains could each register through checkout and create a team with the same name in the same tournament (verified by masquerading as two players who both created "Mac Attack" in tournament 2). The admin-side `+ Create Team` button at `/admin/ccsoccer/tournament/{tournament}/teams` already had a duplicate-name check (`TournamentTeamsController::createTeam` lines 339–352) but the captain-checkout path did not. Two layers of defense added:

- **Form-level block** (`TournamentTeamPane::validatePaneForm`) — queries existing teams in this tournament for a name match (trimmed); blocks submission with the error "Team name already exists for this tournament. Please create a different team name, or join the current team that was already created."
- **Race-condition guard** (`OrderCompleteSubscriber::createTournamentRegistration`) — re-checks immediately before `$team_storage->create()`. On collision logs an error including team name + tournament ID + user ID + order ID, tags the order with `ccsoccer_team_name_collision` data (team name, tournament ID, existing team IDs, user ID, timestamp), and skips team + registration creation so admin can refund the deposit or contact the captain.

**Follow-up worth doing later:** Build an admin view that lists orders with `ccsoccer_team_name_collision` set, with a one-click refund-and-notify action. Race window is small but the failure mode (paid deposit, no registration) is bad UX if it ever fires.

### Sync Team Names from League — New Admin Action ✅ COMPLETE
Closes the Phase 2 gap from the team-name refactor: when a league had no `team_names` taxonomy terms at the time the season was created, or when new team names get added to the league mid-season, there was no way to pick them up without recreating the season. The auto-create hook in `ccsoccer_season_insert()` only fires once on first save.

New admin action added:
- Route `ccsoccer.season_regenerate_teams` at `/admin/ccsoccer/season/{season}/regenerate-teams`, gated by `administer ccsoccer` permission + `_csrf_token` requirement
- `SeasonController::regenerateTeams()` method (TeamManagerService now injected via DI) that calls `TeamManagerService::createTeamsForSeason()` with the default non-destructive behavior, surfaces created/skipped counts via messenger, and redirects back to the season canonical page

**Non-destructive guarantee:** Existing Team entities are never modified or deleted. Player assignments stay. Captain-created teams stay. Manually-added teams stay. Only creates new Team entities for league taxonomy terms that do not yet have a corresponding team in this season. Idempotent — safe to click multiple times.

Subtle edge case to know about: dedup is on the `team_name` taxonomy reference field, not the `name` string. If someone manually created a team via `/admin/ccsoccer/team/add` without setting the `team_name` taxonomy field, the Sync action will create a second Team with the same name (one with the taxonomy link, one without). Admin can delete the duplicate. Could harden later by also matching on `name`.

### Season Page Button Cleanup ✅ COMPLETE
Resolved the long-standing label confusion: the "Generate Teams" button actually opened the Roster Builder (drag-and-drop player → team UI), while the new Sync button creates the Team entities themselves.

- "Generate Teams" → renamed to **Roster Builder** (now matches the destination page heading, the form class name `RosterBuilderForm`, and the URL slug `/roster`)
- New sync button labeled **Sync Team Names from League**
- Reordered so Sync sits before Roster Builder — buttons now read left-to-right in workflow order: define team slots → assign players → schedule games
- Admin menu link title (`ccsoccer.module` line 2562) and `_title` on the `ccsoccer.roster_builder` route in `ccsoccer.routing.yml` also updated to **Roster Builder** for end-to-end consistency
- Help Center "Using the Roster Builder" guide (`HelpCenterController::getRosterBuilderContent()`) updated: references the new button names and adds an optional Sync step with the non-destructive callout

### CreditsPane Cleanup ✅ COMPLETE
Removed the yellow `donate_note` markup block from `CreditsPane::buildPaneForm()` (only rendered when the "donate" radio was selected). Players already choose their action via the three radio buttons (Apply / Keep / Donate); the extra "Thank you! Your donated credits help support the league…" callout added noise without information. CSS class `credits-donate-note` is now unreferenced — could be cleaned from the stylesheets in a future tidy pass; harmless to leave.

### Files Changed This Session
- `web/modules/custom/ccsoccer/ccsoccer.module`
- `web/modules/custom/ccsoccer/ccsoccer.routing.yml`
- `web/modules/custom/ccsoccer/src/Controller/HelpCenterController.php`
- `web/modules/custom/ccsoccer/src/Controller/SeasonController.php`
- `web/modules/custom/ccsoccer/src/EventSubscriber/OrderCompleteSubscriber.php`
- `web/modules/custom/ccsoccer/src/Plugin/Commerce/CheckoutPane/CreditsPane.php`
- `web/modules/custom/ccsoccer/src/Plugin/Commerce/CheckoutPane/TournamentTeamPane.php`

### Still Open / Not Done This Session
- **2b** — Pre-filter `team_name` terms on the manual `/admin/ccsoccer/team/add` form to the season's league. Existing checklist item.
- **2e** — Add league/series columns and filter on the `team_names` taxonomy overview page.
- **`TeamManagerService::createTeamNameTerm()` parameter rename** — `$tournament_id` parameter is now misleadingly named (writes to `field_team_name_series`). No current caller hits this in a broken way, but it's a latent footgun. Rename to `$series_id` and update docblock.
- **Destructive "Wipe and Rebuild Teams" variant** — intentionally out of scope this session. If wanted later, would need a `ConfirmFormBase` confirmation page and call `createTeamsForSeason($season, $clear_existing = TRUE)`.
- **Admin "team-name collision review" view** — list orders with `ccsoccer_team_name_collision` order-data tag, with refund/notify actions. Race is rare but the failure mode is real.

---

## Session Work — May 23, 2026

### Module Updates ✅ COMPLETE
- Drupal core 11.3.9 → 11.3.10 (security update) — deployed to TEST + PROD
- Better Exposed Filters 7.1.1 → 7.1.2 (had schema updates, ran updb) — deployed to TEST + PROD
- WebAuthn 2.0.0-rc7 → 2.1.0-beta1 — **deferred to this weekend** (needs careful auth testing)
- reCAPTCHA 8.x-3.5-rc1 — skipped (RC, not ready)

### Deploy Aliases Fixed ✅
`ccsDeploy` and `ccsProdDeploy` in `~/.bashrc` on server now include `composer install` after `git pull`. Previously composer was never running on deploys, meaning module updates weren't actually applied to the servers. Fixed and confirmed working.

### Migration Welcome Email ✅ COMPLETE
Bulk welcome email system fully built and tested. Queues email to all 1,729 active users.

**To send on go-live day (on PROD):**
```bash
cd ~/public_html/ccsoccer_site
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web ccsoccer:migration-welcome
# Then let cron pick up the queue, or run manually:
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web queue:run ccsoccer_notification
```

**Fixes applied this session:**
- Hardcoded password reset URL to `https://ccsoccer.com/user/password` (was generating `http://default/` in Drush CLI context)
- Updated profile photo copy per Haley's feedback: "Please ensure you choose a pic that clearly shows you (and only you) so we know who you are."

**Logo:** Shows broken in test emails because Yahoo Mail proxy hits ccsoccer.com which is IP-whitelisted. URL is correct — will work post-launch.

**Remaining "CC Soccer":** Tournament deposit and jersey notification functions still say `CC Soccer`. Deferred.

### Pre-Launch Security Checklist ✅ COMPLETE
- ✅ `trusted_host_patterns` — set on both TEST and PROD `settings.local.php`
- ✅ `hash_salt` — set on PROD `settings.local.php`
- ✅ Devel — confirmed uninstalled on PROD; intentionally left on TEST for debugging
- ✅ HTTPS redirect + www → non-www — confirmed in `web/.htaccess` on PROD

### Cron ✅ COMPLETE
- PROD cron added to cPanel: every 15 min via `ea-php83` + drush pointing at `ccsoccer_site`
- Old D7 cron entry (`ccsoccer.com/cron.php?cron_key=...`) deleted
- TEST cron was already in place, left as-is

### Security Metrics ✅ IN PROGRESS
- Four SecurityMetrics scanner IPs whitelisted in PROD `web/.htaccess`
- Waiting on SecurityMetrics to complete scan and provide next steps

### Authorize.net Live Config ✅ RE-ENTERED
- `drush cim` had overwritten PROD payment gateway with sandbox config
- Re-entered live credentials (see Authorize.net section below)
- **Action needed:** Audit full scope of what cim overwrites on PROD (checklist item #1)

### Team Name Taxonomy Refactor ✅ COMPLETE (Phase 1)
- Replaced `field_league` and `field_tournament` on `team_names` vocabulary with `field_team_name_league` and `field_team_name_series`
- Added `TournamentSeries` entity — persistent recurring tournament concept (e.g. "SLO Friendly")
- Added `tournament_series` field to Tournament entity
- Taxonomy term form updated to show league + series fields
- `TeamManagerService` updated to query correct field names
- `ccsoccer_season_insert()` hook now fires regardless of active status; `ccsoccer_season_update()` creates teams when season flips to active
- `OrderCompleteSubscriber::createTournamentRegistration()` now creates a taxonomy term when captain registers and creates a team (term tagged to the tournament series for historical reference)
- **Key finding:** Tournament team creation uses `OrderCompleteSubscriber`, NOT `TournamentTeamManager::createTeam()` or `_ccsoccer_process_tournament_registration()` in ccsoccer.module
- **Still needed (2b/2c/2e):** See checklist item 2 sub-items

### SecurityMetrics Test Product
- Created product type "Security Metrics Test" on PROD via UI (not in config/sync) — caused `cim` failure
- Deleted product + product type via cim after removing DB records
- TODO: Recreate properly — create product type on LOCAL, cex, deploy, then create product entity on PROD

### config_ignore ✅ INSTALLED
- Payment gateway configs now ignored by `drush cim` on all environments
- Payment gateway credentials safe from accidental cim overwrites

### Authorize.net Re-entry Note
- Had to re-enter live credentials after cim overwrote them (before config_ignore was in place)
- Going forward: credentials are safe, cim won't touch payment gateways

---

## Combined Pre-Launch Checklist

### 🚨 Immediate

1. ~~**Caleb** — Audit what `drush cim` overwrites on PROD.~~ ✅ DONE — Installed config_ignore; payment gateways now ignored by cim; manual/nab dev gateways deleted; system.site stays in sync.

2. **Caleb** — Team names: currently associated to season/tournament. Refactor so team name terms are associated to a league or tournament series — when a new season/tournament is created, team names default from the parent. Phase 1 (foundational) complete — TournamentSeries entity, taxonomy fields, tournament_series field on Tournament all installed.
   - 2a. ✅  DONE -- ~~Taxonomy term form shows league + series fields~~
   - 2b. `/admin/ccsoccer/team/add` still needed for admin-created tournament teams; needs UX polish to pre-filter terms by series. Discuss with Andrew.
   - 2c. ✅  DONE -- ~~`TeamManagerService` field names updated; season teams auto-create on season insert/active flip; tournament captain registration creates taxonomy term tagged to series. KEY: tournament team creation is in `OrderCompleteSubscriber::createTournamentRegistration()`, NOT `TournamentTeamManager`.~~
   - 2c (cont). ✅ DONE (May 25) --~~ On-demand "Sync Team Names from League" admin action wired up: new route `ccsoccer.season_regenerate_teams`, new `SeasonController::regenerateTeams()`, button on the season page. Non-destructive (idempotent, only adds Team rows for taxonomy terms that do not yet have a team in this season). Closes the "createTeamsForSeason exists but unclear where it is called from UI" gap. Field-name confusion (`field_league` → `field_team_name_league`, `field_tournament` → `field_team_name_series`) was already resolved by commit `975f51f` on May 23.~~
   - 2d. Phase 3: Roster builder verification
   - 2e. Add league/series columns and filter to team names taxonomy overview (`/admin/structure/taxonomy/manage/team_names/overview`)
   - 2f. ✅  DONE -- ~~Rename `TeamManagerService::createTeamNameTerm()` `$tournament_id` parameter to `$series_id` and update docblock — currently writes to `field_team_name_series` despite the misleading name. Latent footgun, no current caller hits it incorrectly~~
   - 2g. Admin "team-name collision review" view — list orders with `ccsoccer_team_name_collision` order-data tag (set by `OrderCompleteSubscriber` when two captains race on the same team name), with one-click refund-and-notify actions. Race window is small but the failure mode (paid deposit, no registration) is bad UX if it ever fires. See May 25 session for context.

---

### 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 (doing May 23).~~

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

6. **Andrew** — Geo/country blocking via Cloudflare.

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 (contains real user data).

9. ✅ DONE — Pre-launch security checklist (trusted_host_patterns, hash_salt, Devel, HTTPS redirect).

10. ~~**Caleb** — Security Metrics automated testing setup.~~ — SecurityMetrics IPs whitelisted in PROD `.htaccess`; waiting on SecurityMetrics to complete scan and provide next steps.

11. ~~**Caleb** — Configure cron on PROD via cPanel, every 15–30 min.~~ ✅ DONE — PROD cron running every 15 min; D7 cron entry deleted.

---

### 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. **Do this weekend** — test full auth flow after.

16. Backup strategy verification.

17. CSP headers in report-only mode.

---

### Post-Launch / First 72 Hours

18. ✅ DONE — Assign `permanent_override` role (Haley, Layne, Julie, Myk, Kyle).

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: `6LchguosAAAAC5kLFmKj0xCGdEWXNntLacANpVN`
- Secret key: `6LchguosAAAANBJ8ikcrZvYQMxs6-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.

---

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