# LOCAL Test Checklist — Season Invitation Timing Fix

Covers items 1–4 and 6 from the July 18 scope: move accept to order completion
(Bug 1), fix `removeMember()` invitation cleanup (Bug 2), stale-orphan re-invite
fix (Bug 3, narrow), post-payment capacity guard (E2/E6), and the collapsed
`GroupPane` UI with token identity check (E3).

Files touched: `GroupPane.php`, `OrderCompleteSubscriber.php`, `GroupController.php`.
Not yet done: item 5 (data-repair drush command) — separate phase.

**Note on rate limits while testing:** `invite()` is flood-limited to 10/5min,
`nudge()` to 5/10min per user. Repeated manual testing as the same board/manager
account can hit these — space out invite attempts or use `drush php-eval` to
clear flood events if you get blocked mid-test.

---

## A0. Zero-invitation happy path — test this FIRST

Highest blast radius if broken (breaks all season checkout), since
`createSeasonRegistration()` runs on every completed season order regardless
of whether an invitation is involved.

- [ ] Register for a season with no pending invitations at all (a fresh test user, or a season/player combo with no invite in play). Full checkout through payment.
- [ ] Confirm registration is created normally — `group_id`/`invited_by` NULL, `invitation_status = none`, exactly as before this change.

## A. Core flow regression — must still work exactly as before

- [ ] Manager invites an existing user to their season group → invitation created, email sent, status `pending`.
- [ ] Invitee logs in, adds season to cart (no token), reaches Group Invitations pane → sees "Join X's group" + "Figure out group later" radio options.
- [ ] Selects "Join X's group," completes checkout through payment.
- [ ] **Check DB directly** (`drush sqlq "SELECT id, status FROM ccsoccer_invitation WHERE id = X"`): status is `accepted` only *after* order completion, not at pane-submit time.
- [ ] Registration created with correct `group_id` / `invited_by` / `invitation_status = accepted`.
- [ ] Manager's Manage Group page shows the new member; other pending invitations to that invitee (if any) are now `declined`.

## B. Bug 1 fix — the actual point of this work

- [ ] Manager invites a player. Player logs in, reaches Group pane, selects "Join X's group," clicks Continue (submits pane) — **then abandons checkout** (close tab, don't pay).
- [ ] **Check DB:** invitation status is still `pending` (this is the core assertion — under the old code this would already read `accepted`).
- [ ] Player logs back in later (no completed order): invitation still shows on My Registrations / register-page banner, still selectable.
- [ ] Player completes registration normally later via the same still-pending invitation → accepts correctly at that point.

## C. Token link + identity check (E3)

- [ ] Manager sends a magic-link invite (token) to an email with no account yet.
- [ ] Click the link → redirected through login/register → season lands in cart → Group pane shows "Join X's group" **pre-selected by default**.
- [ ] Complete checkout → accepted correctly, same as path A.
- [ ] **Identity check:** forward that same magic link to a *different* email address, log in as that different account, visit the same link/pane. Confirm the invitation does **not** appear pre-selected — the pane should show whatever that account's own genuine invitations are (or "no invitations" if none). This confirms E3 is closed: a mismatched email gets no benefit from the token.

## D. Back-button behavior (E1)

- [ ] Player has 2+ pending invitations for the same season. Select invitation #1 on the Group pane, click Continue.
- [ ] Click Back to return to the Group step, then Continue again.
- [ ] Confirm invitation #1 is still selected (not reset to #2 or "none").
- [ ] Complete checkout → the invitation actually accepted is #1, matching what was shown.

## E. Post-payment capacity guard (E2/E6) — the trickiest to trigger by hand

The guard only fires when the group's **confirmed** member count is already
at or above `max_group_size` at the exact moment a pending invitee's order
completes. Under normal single-user testing this is hard to hit naturally,
since the guard's own math never lets confirmed count exceed max — it just
prevents *going past* it. Two ways to force it on LOCAL:

- [ ] **Shortcut method:** invite a player (group has room, invitation goes to `pending`), then before they complete checkout, lower the season's `max_group_size` via the admin season edit form to below the group's current confirmed count. Complete the invitee's checkout → confirm it does **NOT** join the group, invitation stays `pending`, and the order gets flagged (`drush sqlq` or check the order's data for `ccsoccer_group_capacity_exceeded`).
- [ ] **Closer-to-real method:** invite two players to a group with `max_group_size` small (e.g. 2, so manager + 1 more is full). Have one accept instantly via My Registrations (`acceptSeasonInvitation()` — this path has no capacity check, a known separate gap, not part of this fix). Then complete the second player's pending checkout → same expected result as above.
- [ ] Either way, confirm the player's registration is still created (payment captured) — just without a group — so nothing is silently lost.

## F. `removeMember()` invitation cleanup (Bug 2) — both season and tournament

- [ ] **Season:** manager invites + accepts a player through checkout (per section A). Manager removes them via Manage Group.
- [ ] Check DB: the removed player's invitation status is now `declined` (not left `accepted`).
- [ ] Manager re-invites the same player → succeeds without an "already accepted" block.
- [ ] **Tournament:** captain invites + accepts a player onto the team (token_accept or join). Captain removes them via the roster/manage-team page.
- [ ] Check DB: same — invitation flips to `declined`, captain can re-invite without being blocked.

## G. Bug 3 — stale orphan no longer blocks re-invite

Since Bug 1 is fixed, this scenario shouldn't occur going forward — this test
simulates a leftover orphan (like the ones already patched in PROD) to confirm
the narrow fix works if one ever recurs (e.g. a mid-request crash).

- [ ] Manually set an invitation to `status = accepted` for an email with **no** matching registration in that group (`drush sqlq` or a quick script).
- [ ] Manager tries to re-invite that same email → should succeed, new pending invitation created, no "already accepted" block.
- [ ] **Contrast case:** do the same but WITH a real matching registration this time (genuine member). Re-invite attempt should still correctly block with "already accepted."

## H. Tournament flow untouched (sanity)

- [ ] Full tournament registration end-to-end (create team / join / token_accept / pool) — confirm nothing regressed, since `OrderCompleteSubscriber.php` was touched but only inside `createSeasonRegistration()`, not `createTournamentRegistration()`.

---

## Priority if time is short

Test in this order, don't skip A0–B: A0 (zero-invitation path) → B (Bug 1
proof) → A (core accept flow) → F, G, C, D → H → E last (safety net, not
launch-blocking).

## After LOCAL passes

- Commit, push to `main`, deploy to TEST (`ccsDeploy && ccsCr` — no cim/updb).
- Caleb + Andrew both test on TEST before PROD.
- Item 5 (data-repair drush command) — next phase, not blocking this one.
