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

Earlier session detail (June 23 – July 5) archived in `archive/SESSION_HANDOFF_2026_07_08.md`. July 17–18 session (Andrew's four audit docs + fixes below) archived in `archive/SESSION_2026-07-18.md` before this update.

---

## Session Work — July 18, 2026 (later) — Review of `fix/invitation_flow_analysis` (Caleb + Claude)

Reviewed Andrew's branch against the original ask (Brent/Myk group-invitation bugs). Findings:

- **Bug 1 (Brent — early accept in `GroupPane::submitPaneForm`) and Bug 2 (Myk — `removeMember()` doesn't decline the invitation) are both still OPEN.** Neither file is touched anywhere in this branch. Andrew's work is real and adjacent (D1 refund bug, Bug 4 email delivery, S2 tournament auto-decline, T6/R1 admin-tooling fixes below) but does not fix, and does not put at risk, the invitation-timing bug itself. That's still a separate PR — scope per `INVITATION_FLOW_ANALYSIS.md` Section 5 (move accept to order completion, rebuild pane from order-data selections, fix `removeMember`, data-repair migration for already-orphaned `accepted` rows).
- Reviewed the shipped fixes (S2, D1, Bug 4, T6, R1) line-by-line for regressions — none found. Each is narrowly scoped and matches its doc entry.
- Removed leftover `DEBUG:` notice-level logging in `NotificationService::send()` (not Andrew's — pre-existing noise burying the real signal in dblog, called out as H1 in `ORDER_FLOW_EDGE_CASES.md` but never fixed). Lint-checked clean.

**Plan:** merge `fix/invitation_flow_analysis` (+ the debug-log commit) into `main`. No `cim`/`updb` — zero `.install` or config `.yml` files anywhere in the branch's diff.

**Carried forward, still open regardless of this merge:**
- Andrew to manually refund Avi's $133 (order 87, Payments tab) and email him — flagged below, not yet confirmed done.
- The July 17+18 LOCAL test checklist below — not yet confirmed run.
- `drush ccsoccer:send-pending-email-invites --dry-run` then live on PROD (Bug 4 data repair) — not yet run.

---

## Session Work — July 18, 2026 — Admin tooling follow-ups (Claude), UNCOMMITTED

Worked through `ADMIN_TOOLING_REVIEW.md` findings with Andrew. Three resolved **as designed** (intent now documented in code so future reviews don't re-flag), two **fixed**, two **on hold**. All uncommitted, on top of the July 17 changes. Deploy: code only, `drush cr`.

### As-designed — documented in code + UI
- **TS1 (tournament format):** builder is purpose-built for the single-day SLO Friendly round-robin — no brackets/pools/scores/multi-day. Scope docblocks on `TournamentScheduleGeneratorService` + `TournamentScheduleBuilderForm`; info notice now renders atop the schedule builder; Tournament `format` field options labeled "(not implemented)" for bracket/pool_play (options kept so existing data stays valid).
- **Decision #2 (destructive regenerate):** by design for the pre-season board-meeting workflow (generate candidates → snapshot → restore winner → publish). By-design comments at `clearSchedule()` in both generators. **Hardened both `restoreSnapshot()`s:** restoring into an empty grid (after Clear All Games) now errors with "generate first, then restore" instead of a false success; partial restores WARN with the count of snapshot entries that had no matching slot. Operating rules recorded in the review doc: finalize teams/slots/fields/dates before generating candidates; no Clear All Games mid-meeting; never regenerate after week 1.
- **S3 (jersey colors) — original finding was WRONG-way-round; league is Red vs White, no home/away mentality.** Grid (red top row / white bottom), next-game banner, and iCal feed were all consistent: home_team slot = Red. Only the Game entity docblocks were backwards. Fixed the docs (home_team = the Red team, away_team = the White team, with a do-not-"fix" warning comment) and renamed the one player-facing home/away verbiage — `exportPdf` column headers → "Red Team"/"White Team". Admin field labels left as Home/Away Team (renaming ripples through admin forms; descriptions explain the mapping).
- **S7 (red/white coin flip per game):** as designed — flip only decides jersey color for the night; no balance tracking wanted. Comment at the flip in `ScheduleGeneratorService`.

### Fixed
- **T6:** unguarded `new \DateTime($dob)` in `TournamentRosterBuilderForm` — one malformed DOB (D7-migration risk) 500'd the whole roster builder. Now try/catch → default age 30 + watchdog warning naming the player. Sweep confirmed every other DOB parse in the module was already guarded; this was the one drifted site.
- **R1:** admin Teams list showed "0 players" on every season team (counted `Team.players`, which the season roster builder never writes — season truth is `Registration.team`). Season mode now counts registrations (`status IN paid/active` — cancelled players drop out automatically); tournament mode keeps `Team.players`. Deliberately no dual-write (that pattern caused the tournament ghost-member desyncs).

### On hold — Andrew deciding, NO changes made
- **S4/TS3 (drag-swap validation):** blocking is NOT wanted — mid-rearrangement legitimately passes through broken states, and the workbench was built as the staging area to avoid the hard block direct moves have. Proposed design recorded in the review doc (`findScheduleConflicts()` + non-blocking warning banner + single confirm at publish); awaiting Andrew's decision. Note current inconsistency: season moves hard-block, swaps check nothing, tournament checks nothing.

### Files touched July 18 (all in `web/modules/custom/ccsoccer/`)
`src/Service/ScheduleGeneratorService.php`, `src/Service/TournamentScheduleGeneratorService.php`, `src/Form/TournamentScheduleBuilderForm.php`, `src/Form/TournamentRosterBuilderForm.php`, `src/Entity/Tournament.php`, `src/Entity/Game.php`, `src/Controller/ContentController.php`, `src/TeamListBuilder.php`, plus `ADMIN_TOOLING_REVIEW.md`.

### Still open from the small-fixes list
G4/G5 (cancelled games render as normal in public per-season iCal + printable PDF — per-user feed is correct), G3 (3 pm cancellation-reminder cron only fires if a tick lands in 15:00–15:04, plus unlocked read-modify-write dedupe race).

### LOCAL test checklist for July 17+18 changes (before PROD)
1. Refund path: cancel a test season registration with Commerce refund on TEST gateway → succeeds, payment flips to Refunded. **(Also: Andrew to manually refund Avi's $133 — order 87 Payments tab → Refund — and email him.)**
2. Email-only invite → Mailpit email w/ working link (board address); non-allowlisted address → warning message, Send Reminder available.
3. Tournament reg with pending team invite via "no team yet"/pool → invitation stays pending.
4. Snapshot: save → Clear All Games → restore = instructive error; regenerate → restore = schedule back.
5. Roster builder: bad DOB row renders (age 30 + watchdog warning); `/admin/ccsoccer/teams` season mode shows real counts.
6. Schedule builder shows the single-day scope notice.

---

## Session Work — July 17, 2026 — Audits (Claude) + 3 fixes, UNCOMMITTED on main working tree

### Four review documents produced (repo root) — reference these when reviewing PRs

1. **`INVITATION_FLOW_ANALYSIS.md`** — all season/tournament invitation+acceptance paths documented. Confirms Caleb's hypothesis on the Brent/Myk data issues, with one refinement: the early-accept happens in `GroupPane::submitPaneForm` (checkout step 2), not the token link itself. 4 bugs + 7 edge cases + **Section 5 = review checklist for Caleb's upcoming invitation PR** (incl. required data repair for orphaned `accepted` invitations). Tournament pane already does it right (defers accept to order completion) — the fix pattern is in our own codebase.
2. **`ORDER_FLOW_EDGE_CASES.md`** — order pipeline silent failures. Top items: transactional emails (receipts, invitations) suppressed by notification preference `none`/`text`-without-phone; flagged "paid but broken" orders still have no admin surface (checklist item 18 — now live-critical); waitlist offers never expire (entry stuck `offered`, next-in-line never offered, reserved spot held forever); DEBUG noise in `send()` burying real errors in dblog.
3. **`SEASON_TOURNAMENT_DRIFT_AUDIT.md`** — systematic twin-pair comparison (season↔tournament). 20 divergences, 3 critical: D1 refund 100x (fixed today, see below), D2 admin-accept of tournament invitation crashes on a nonexistent `tournament` field query (`GroupInvitationsForm.php:1279`), D3 tournament order completion never re-checks active/visible (season's "Finding F" guard was never ported). D4: co-captains see Remove buttons but every click fails.
4. **`ADMIN_TOOLING_REVIEW.md`** — roster builders + schedule generators + game-day ops (~45 findings) ahead of first board use this fall. **Three board decisions needed:** (a) tournament schedule builder has NO pool/bracket support, no score fields, single-day only — decide format plan for fall tournaments NOW; (b) regenerate on both schedule builders hard-deletes all game history — operating rule until fixed: snapshot before regenerate, never regenerate after week 1; (c) cancellation notices don't reach preference-`none` players and don't work at all for tournament dates. Also: admin Teams list shows "0 players" for season teams (reads wrong field), jersey colors inverted in player schedule (verified), one bad DOB 500s the tournament roster builder.

### Production incident — order 87 (Avi, $133, cancelled Jul 8) — VERIFIED refund never happened

Season cancel form's "Refund via Commerce" requested a 100x amount ($13,300); Commerce rejected it; registration cancelled anyway; **no money moved, no email sent to player**. Payments tab shows "Refunded: $0.00"; credits $0.
- **TODO Andrew:** refund $133 via order 87 → Payments tab → Refund button (Commerce core form, safe) → verify payment state flips to Refunded → email Avi (he was never notified).

### Three code fixes made today — UNCOMMITTED, all independent, none touch Caleb's season/GroupPane territory

1. **D1 — refund 100x** (`src/Form/CancelRegistrationForm.php`): dropped the `* 100` (Price takes dollars — tournament forms were already correct); reject $0 refunds; failure message now says explicitly that no money moved + manual remediation steps. **Deploy first, own commit — live money bug.**
2. **BUG 4 — email-only invitees never got the invite email** (`src/Service/NotificationService.php` + `src/Controller/GroupController.php` + `src/Drush/Commands/CcsoccerCommands.php` + `templates/ccsoccer-group-manage.html.twig`): new `sendInvitationEmail()` (raw address, reply-to inviter, board allowlist still applies); wired into `invite()`/`nudge()`; `notified` now stamped only after successful send (failure shows warning, invite stays and Send Reminder works immediately); template guarded against empty `notified` (was fataling — found in Andrew's LOCAL test). **After deploy run `drush ccsoccer:send-pending-email-invites --dry-run` on PROD, then live** — repairs existing pending email-only invites (Matt Willis et al.).
3. **S2 — tournament registration silently declined ALL pending team invitations** (`src/EventSubscriber/OrderCompleteSubscriber.php`): decline now gated on `if ($team)` — pool/"no team yet" registrants keep invitations pending (mirrors season's `invitation_status === 'accepted'` guard); also un-breaks the capacity-exceeded fallback's leave-pending promise.

Deploy for all three: code only, no cim/updb, `drush cr` in each environment. LOCAL test notes are in each doc next to the ✅ IMPLEMENTED markers. Syntax-validated (PHP 8 parser); not yet run through a full LOCAL regression — test before PROD.

### Coordination with Caleb
- His invitation-timing PR: evaluate against `INVITATION_FLOW_ANALYSIS.md` Section 5 (esp.: accept must move to order completion, pane must rebuild from order-data selections not pending status, `removeMember` invitation cleanup, and the data-repair migration — code fix alone leaves prod data inconsistent).
- Today's fixes touch `GroupController::invite()/nudge()`, `OrderCompleteSubscriber::createTournamentRegistration()`, `NotificationService` — flag to him to rebase/merge before his PR if he's in those files.

---

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