# TEST Spot Checks — Aug 4/5 roster-sync + D-8 deploy

**Date:** August 5, 2026
**Covers:** PR #124 (six roster-sync fixes, Aug 4) **and** the D-8 package (CF10 + B + E, Aug 5).
**Purpose:** confirm the deploy is sound on TEST before PROD. **This is a spot-check pass, not a
re-run of the LOCAL suite.**

> ### What this is, and what it is not
>
> The exhaustive logic testing already happened on LOCAL — plan §10.8 (passed Aug 5) and
> `archive/GROUP_CLEANUP_BRIEF.md` §9's 29 cases. **Re-running all of that here would mostly re-prove things
> that do not vary by environment.**
>
> **What TEST is actually for:** the container rebuild, config drift, real data shapes, notification
> gating, and the handful of paths that could not be exercised properly on LOCAL. Those are what this
> document covers. It should take about **60-90 minutes**, most of it in §2.
>
> **This is a disposable working document.** Archive it after the PROD deploy — it is a deploy record,
> not a status doc, and `archive/DOC_CLEANUP_CONSOLIDATION_WIP.md` should not have to account for it long-term.

---

## What is being deployed

| Cluster | Commits | Risk |
|---|---|---|
| **PR #124** — CF1, CF1b, CF2 + D2, CF12, CF4, CF8, CF5 | `e5317f8` `59c12ec` `8156bc1` `b46c28b` `b22734f` `7c9a7c5` | LOCAL-tested Aug 5. **Closes a live PROD fatal** (D2). Four constructor changes |
| **D-8** — CF10 + B + E | `dde90b5` `995e286` + commit 3 | Written and linted Aug 5, **click-tested on LOCAL only**. Three more constructor changes, thirteen new call sites |

**Seven constructor/service changes across the two clusters.** That is the single most likely way this
deploy breaks, and it breaks loudly — see §0.

---

## Before you start — four TEST-specific hazards

1. **⚠ Check `site_instance` before generating any invitation.** `archive/ORDER_FLOW_EDGE_CASES.md` H2:
   `Settings::get('site_instance', 'production')` defaults to **production** if the line is missing from
   `settings.local.php`. TEST carries real PROD SMTP credentials and a copy of real player data, so a
   TEST box missing that line **emails real players**. Confirm it is non-production *first*:
   ```bash
   drush php:eval "var_dump(\Drupal\Core\Site\Settings::get('site_instance', 'production'));"
   ```
2. **Notifications are gated on TEST** — only board members and beta testers receive. Do not treat
   "no email arrived" as a failure for a non-allowlisted recipient. Notification *content* was verified
   on LOCAL via Mailpit; here you are only confirming that sending does not throw.
3. **Flood limits apply.** `invite()` is 10 per 5 minutes per user, `nudge()` 5 per 10 minutes. Repeated
   manual testing as the same manager will hit these. Use several accounts or wait it out.
4. **Pre-existing stranded groups will start clearing themselves.** There is no data repair in this
   change, but §9.4 query A's population will shrink as soon as anything touches those groups. **That is
   expected behaviour, not a bug.** Record the query A count *before* you touch anything (§1) so the
   movement is explicable.

---

## 0. Deploy and container — do these first

If §0 fails, stop; nothing below is meaningful.

- [ ] **0.1** Deploy: `ccsDeploy && ccsCr`. Code-only — **no `updb`, no `cim`**. No update hooks, no
      exported config changed. `ccsoccer.services.yml` is module code, not exported config.
- [ ] **0.2** **`drush cr` completed without error.** Mandatory. Seven constructors/services changed
      across the two clusters (`TournamentCancelRegistrationForm`, `GroupInvitationsForm` ×2,
      `CancelRegistrationForm`, `GroupController`, `OrderCompleteSubscriber`, `RosterBuilderController`,
      plus the new `ccsoccer.group_dissolve`). **Without a rebuild the container is stale and these
      pages fatal outright** — that is the failure mode to expect if anything went wrong.
- [ ] **0.3** `drush config:status` — expect **only** the three known `media_library` display entries.
      Anything else is drift from something unrelated; investigate before continuing.
- [ ] **0.4** **Smoke the pages that would fatal on a stale container.** Each should load, no WSOD:
      - `/my-registrations`
      - a season Manage Group page
      - `/admin/ccsoccer/group/{group_id}/invitations` for a season group
      - the season Roster Builder
      - a tournament Manage Team page
- [ ] **0.5** `drush watchdog:show --count=50` — note anything already present so §5's comparison is
      clean.

---

## 1. Read-only baseline — run before touching anything

Two minutes, no writes, and it is the only chance to capture the "before" numbers.

- [ ] **1.1** Run **`archive/GROUP_CLEANUP_BRIEF.md` §9.4 query A** (solo season groups). **Record the count:**
      `________`
- [ ] **1.2** Run **query B** (solo groups with *no* pending invitation — the genuinely stranded ones).
      **Record:** `________` — this is the number that should trend to zero over the following weeks.
      **It is the entire safety net for this design.**
- [ ] **1.3** Run **query C** (groups with zero live members). **Record:** `________`. Do **not** expect
      zero — there is no data repair. You are establishing that it does not *rise*.
- [ ] **1.4** Run **query D** (pending invitations whose group has no live member). **Record:** `______`
- [ ] **1.5** **Run `ROSTER_DATA_AUDIT.sql` on TEST.** Still never run — it has been outstanding since
      July 28. Save the section 1 summary. This is the best opportunity to get real numbers for the
      mismatch classes against realistic data.
- [ ] **1.6** Confirm `drush role:list` shows **no role other than `administrator`** holding
      `manage seasons` or `generate rosters`. This closes the one open caveat on **Q-B9**
      (`archive/GROUP_CLEANUP_BRIEF.md` §11) — the answer there rests on `config/sync`, and there is
      unexplained role drift on record from June 16.

---

## 2. D-8 season group lifecycle — the new code, highest risk

This is where the deploy is most likely to be wrong, because it is the least-exercised code. **Check
`Registration.group_id` in the database after each, not just the screen.**

**Setup:** three test registrations in one open season — call them P1, P2, P3.

### The headline case

- [ ] **2.1** **P1/P3, via My Registrations.** P1 invites P2; P3 invites P2; P2 accepts P1's from My
      Registrations. → P3's invitation auto-declines *(as it did before)* **and P3's `group_id` is now
      cleared**. Then **P1 invites P3 → succeeds**, where it previously said *"already in another group
      for this season."* P3 accepts → group of three.
      *This is the whole point of the change. It used to require an admin.*
- [ ] **2.2** **Same case via the Register page** (magic link, `RegistrationController` path). Same
      assertions. **Both Accept buttons must be checked** — they route through different controllers and
      both were broken by the Aug 4 bug for exactly this reason.

### CF10 — the mint no longer happens on a failed invite

- [ ] **2.3** Registered player with no group invites an address that resolves to nobody → error, **and
      their `group_id` is still NULL.**
- [ ] **2.4** Invite someone already in another group → *"already in another group for this season"*,
      **inviter's `group_id` still NULL.**
- [ ] **2.5** **Happy path and the off-by-one.** Valid invite → `group_id` set as before, invitation
      carries it. **Then send a second invite from the same manager → reuses the same `group_id`, no new
      UUID, and the group-size number on the Manage Group page reads the same as it did before the
      deploy.** *If that count is one low, CF10's size fix did not take.*

### E and the carve-out

- [ ] **2.6** **Delete the only invitation.** Manager invites, then deletes → **`group_id` cleared**,
      the "you are no longer in a group" message appears, and the manager can now be invited by someone
      else.
- [ ] **2.7** **The carve-out.** Manager sends **two** invitations, deletes one → **group NOT cleared**,
      the other still pending. *This is what stops B dissolving groups out from under people, and it is
      the objection that kept option B off the table in July.*

### Route 4 — the group empties

- [ ] **2.8** Group of two, member clicks **Leave Group** → **manager's `group_id` cleared.**
- [ ] **2.9** Group of three, manager removes one member → **group of two survives, nothing cleared.**
      Remove the second → **cleared.**
- [ ] **2.10** **Admin cancels a member's registration** in a group of two → **manager's `group_id`
      cleared.** *(Cancellation is administrator-only; there is no player-facing cancel route — Q-B9.)*
- [ ] **2.11** **Manager cancels** a group of three → CF8's existing dissolve, unchanged. All three
      cleared, **and `Registration.team` is untouched for everyone.** Check before and after.

### Admin surfaces — where every missed call site was found

- [ ] **2.12** **Admin declines the last pending invitation.** Solo manager with one pending invitation
      → admin flips the **invitation** row to `declined` on the Group Invitations page and saves →
      **group cleared.** *This is the site the first draft of the brief missed; a call placed after the
      member loop instead of after the third pass passes 2.13 and fails this.*
- [ ] **2.13** **Admin declines a member row.** Group of two → admin flips the **member** row to
      `declined` → **manager's group cleared.**
- [ ] **2.14** **Roster Builder merge.** P3 has invited P6. Admin drags P6 into P1's group in the season
      Roster Builder → **P3's invitation is declined and P3's group cleared.**
      **⚠ This is the one behaviour change beyond cleanup** — admin placement did not previously decline
      anything. Confirm it does not surprise you before it reaches PROD.
- [ ] **2.15** Repeat 2.14 for the **"create a group from two players"** drag. Both players' other
      pending invitations should decline.

### Idempotency

- [ ] **2.16** Trigger a dissolve, then trigger the same path again → no error, **no second
      `"Group @gid dissolved"` watchdog line**, and the "you are no longer in a group" message does
      **not** reappear.

---

## 3. PR #124 — and the operating-rule gate

**§3.1 is the gate that lifts the standing operating rule.** It is the highest-value item in this
document after §2.1, because it closes a live PROD fatal.

- [ ] **3.1** **Admin accepts a pending tournament invitation** on the Group Invitations page →
      **no error page** (this fataled before D2), player appears in `Team.players`, `Registration.team`
      set, competing invitations declined, nothing half-committed.
      **Passing this on TEST is what clears the way to lift the "admin Group Invitations page is
      read-only for tournament groups" rule — but the rule only actually lifts once this is on PROD.**
- [ ] **3.2** Admin **declines** a tournament member → removed from `Team.players` **and**
      `Registration.team` cleared; the team page roster shrinks by one.
- [ ] **3.3** **CF12 / the Save trap.** Open the Group Invitations page for a tournament group. Member
      rows must **not** render as "Pending" for paid players who are on a team, and the stats bar must
      agree with the rows. Press **Save with no edits** → *"No changes were made."* and nothing is
      written.
- [ ] **3.4** **Promote a post-June-26 registrant to Captain.** The option must be **present** in their
      dropdown — it was silently absent for every such player before CF12.
- [ ] **3.5** **CF1 — cancel a tournament player** who is on a team → removed from `Team.players`,
      `reg.team` cleared.
- [ ] **3.6** **CF1 — cancel a co-captain** → **`Team.co_captain` cleared, not left dangling**, and
      `Team.captain` untouched. *This is the case CF1's original brief missed entirely.*
- [ ] **3.7** **CF4 — the split-brain reproduction.** Register for a tournament → captain invites →
      accept → **admin cancels** → **register again** → re-invite → accept. → The accept lands on the
      **live** registration. Captain's Manage Group, Tournament Teams, My Registrations and the Roster
      Builder **all agree**. No *"This registration has been cancelled."*
      *This is the bug that started the whole cluster. Run it on both Accept buttons if time allows.*
- [ ] **3.8** **CF5 — roster builder with dirty data.** Needs a deliberately constructed ghost: a uid in
      a team's `players` whose `reg.team` is empty or points elsewhere. Drag them → the stale entry
      clears off **every** team.

---

## 4. Negative and regression checks

The things that must **not** have changed.

- [ ] **4.1** **Tournament regression — the whole lifecycle.** Captain invites → accept → remove →
      member leaves → cancel. **`Team.group_id` remains authoritative, nothing is dissolved, no season
      cleanup fires anywhere.** The D-8 change must be completely invisible to the tournament flow.
- [ ] **4.2** **`groups_locked` ON — player paths still refused.** Invite refused, leave refused, remove
      refused, as before. No dissolve fires.
- [ ] **4.3** **`groups_locked` ON — admin paths still complete.** An admin cancelling a member, or
      declining a member row, **still dissolves.** This is correct and deliberate (**Q-B9**): the lock
      means *players* cannot modify groups, and admin paths have always bypassed it.
      *If someone "fixes" this later, they will have broken an admin's ability to clean up a group they
      just emptied.*
- [ ] **4.4** **`Registration.team` unchanged** across everything in §2. Dissolving a friend group must
      never un-assign anyone from a season team (CF6).
- [ ] **4.5** **Non-manager deletes an invitation they sent** from within a group of three → **nothing
      cleared**, group intact.
- [ ] **4.6** A season group at max size still blocks a further invite, and a player who hits that error
      keeps `group_id` NULL.

---

## 5. Checkout — the money path

**Hardest to test, highest consequence.** `OrderCompleteSubscriber` is the one D-8 call site that runs
inside Commerce's order transition, after payment is captured.

- [ ] **5.1** Confirm TEST has a working sandbox payment gateway before attempting this.
      `commerce_payment.commerce_payment_gateway.*` is in `config_ignore`, so TEST carries its own.
- [ ] **5.2** **Rival decline at checkout.** P2 is not yet registered; P1 and P3 both invite them. P2
      registers and pays with P1's invitation selected in `GroupPane`. → **the order completes
      normally**, P3's invitation declines, **and P3's group is cleared.**
- [ ] **5.3** **Confirm no exception escaped.** `drush watchdog:show --severity=Error`. The cleanup call
      is wrapped in `try/catch(\Throwable)` precisely so a stranded group can never turn into a failed
      order on a charged card — if the catch fired, there will be a
      `"group cleanup for invitation @inv failed"` line. **An entry there is not a blocker for the
      order, but it is a blocker for PROD.**
- [ ] **5.4** If 5.1 is not possible on TEST, **say so explicitly in the PR** rather than letting it
      look tested. This path would then be first exercised on PROD, which is worth knowing in advance.

---

## 6. Soak

- [ ] **6.1** Leave TEST alone for a day of normal use.
- [ ] **6.2** `drush watchdog:show --count=100 --severity=Error` — **no new errors** beyond the §0.5
      baseline across a full register → invite → accept → remove → cancel cycle.
- [ ] **6.3** **Re-run §9.4 queries A, B and C.** Compare against §1:
      - **A and B may have fallen** — expected, as touched groups clean themselves up.
      - **C must not have risen.** A rise means new zero-live-member groups are being created, which
        would be a real defect.
- [ ] **6.4** Re-run **§10.8 items 1 and 4** as a final regression: the CF4 split-brain reproduction and
      the admin accept. Those cover the two live bugs this whole cluster exists to close.

---

## Go / no-go for PROD

**Blockers — do not deploy to PROD if any of these fail:**

- §0.2 `drush cr` clean and §0.4 all pages load
- §2.1 the P1/P3 headline case
- §2.12 admin declines the last pending invitation
- §3.1 admin accept of a tournament invitation does not fatal
- §4.1 tournament regression clean
- §5.3 no escaped exception on the order path
- §6.2 no new watchdog errors after soak

**Not blockers, but record the answer in the PR:**

- §5.1 if checkout could not be exercised on TEST
- §1.2 query B's starting count — needed to judge the trend after PROD
- §2.14 whether the new Roster Builder decline behaves as you want in front of real data

**Still outstanding regardless of this checklist:**

- ⬜ **Add §9.4 query B to `ROSTER_DATA_AUDIT.sql`.** It is the only thing that will reveal a missed
  fourteenth call site, and it should land with this code rather than after it.
- ⬜ **Caleb's review** of `archive/D8_OPTIONS_COMPARISON.md` and `archive/GROUP_CLEANUP_BRIEF.md`.

---

## Rollback

**Code-only, no data step** — this is the sharpest contrast with the option D that was not taken.

Everything the D-8 change writes is a *clearing* of `group_id` / `invited_by` / `invitation_status` to
the values any ungrouped player already holds. Revert the code and those players are simply not in a
group, which the pre-change code has always handled correctly.

- **Revert commit 3** (B + E) → cleanup stops happening. Groups already cleared stay cleared, correctly.
- **Revert commit 2** (CF10) → invite-time minting returns. Nothing else depends on it.
- **Revert PR #124** → the D-8 package depends on `GroupDissolveService` and `LiveRegistrationTrait`
  from it, so this is the one that cannot be reverted alone. Revert D-8 first.

**Nothing needs repairing on the way out.**

---

## Where these checks came from

Consolidated so they stop being scattered:

| Source | What was pulled |
|---|---|
| `ROSTER_RECONCILIATION_PLAN.md` §10.8 | §3.1-3.8, §6.4 |
| `ROSTER_RECONCILIATION_PLAN.md` §10.3 per-fix "Test on LOCAL" blocks | §3.5, §3.6, §3.8 |
| `ROSTER_RECONCILIATION_PLAN.md` §10.5, §10.7 | §0.1, §0.3, §6 |
| `archive/GROUP_CLEANUP_BRIEF.md` §9.1 | §0.2, §1 |
| `archive/GROUP_CLEANUP_BRIEF.md` §9.2 cases 1-22 | §2.1-2.15 |
| `archive/GROUP_CLEANUP_BRIEF.md` §9.3 cases 23-29 | §2.16, §4 |
| `archive/GROUP_CLEANUP_BRIEF.md` §9.4 | §1.1-1.4, §6.3 |
| `archive/GROUP_CLEANUP_BRIEF.md` §11 Q-B9 | §1.6, §4.3 |
| `archive/GROUP_CLEANUP_BRIEF.md` §13 | Rollback |
| `archive/ORDER_FLOW_EDGE_CASES.md` H2 | The `site_instance` warning |
| `INVITATION_FIX_LOCAL_TEST_CHECKLIST.md` | The flood-limit warning |
