# Tournament Nudge 500 — Diagnosis & Ready-to-Apply Fix (D14)

**Date diagnosed:** July 19, 2026
**Status:** NOT YET APPLIED — fix is written and validated below, deliberately held to coordinate with Caleb's invitation-timing PR. No working-tree changes were made.
**Severity:** Live 500 on every "Send Reminder" click for tournament team invitations. Season nudges are unaffected.

---

## Symptom

Clicking **Send Reminder** on a pending tournament invitation (My Group page, e.g. `/my-group/5093`) lands on Drupal's fatal error page:

```
POST https://ccsoccer.com/invitation/29/nudge?token=...
"The website encountered an unexpected error. Try again later."
```

Observed July 19 on invitations 27 and 29 (SLO Friendly tournament team invites: Eileen Amaral, Eileen "Odie" Vavra, Jordan Kennedy, Rachael Lyons, Ryan Walter).

**Important operational note:** the crash happens *after* the email is sent and `notified` is stamped. Reminders that showed the error page most likely **did send** — do not re-send manually. Evidence: the two Eileen rows flipped to "Can nudge in 48h" after the error, meaning `notified` was written before the fatal.

## Root cause

`GroupController::nudge()` final "redirect back" block (≈ lines 1170–1184) looks up the inviter's registration by `season` unconditionally:

```php
// Find the registration to redirect back
$season_id = $invitation->get('season')->target_id;   // NULL for team invites
$my_reg = $this->entityTypeManager->getStorage('ccsoccer_registration')
  ->loadByProperties([
    'player' => $this->currentUser->id(),
    'season' => $season_id,                            // NULL → throws
  ]);
```

Tournament team invitations store `team`, not `season`, so `$season_id` is NULL. In Drupal 10/11, passing NULL as an entity-query condition value **throws** (core requires `notExists()`/`IS NULL` operators instead) → uncaught exception → 500.

This is exactly **D14 in `archive/SEASON_TOURNAMENT_DRIFT_AUDIT.md`** ("Nudge redirect never adapted for tournaments"). The audit predicted a soft wrong-page redirect; the real behavior is a fatal. Update D14's wording when fixing.

### Why it surfaced now (not caused by the July 17 commits)

The broken block predates recent work (present since `05d7654` / `b7cc742`). Commit `7dc6163` (BUG 4 fix, July 17) made email-only invitees actually receive invitation/reminder emails, which made Send Reminder worth clicking on tournament invites for the first time. `94838cf` (DEBUG log removal) is unrelated and benign — verified.

Every code path through `nudge()` for a tournament invite hits the broken block — including the "please wait 48h" throttle path — so retries also 500.

## The fix

Mirror `deleteInvitation()` (≈ lines 1202–1228), which already handles both branches correctly. Replace the block above (the code between the `}` closing the 48h else-branch and the end of `nudge()`) with:

```php
    // Find the registration to redirect back. Team invites have no season
    // (D14 fix — passing the resulting NULL to loadByProperties() throws,
    // which 500'd every tournament nudge). Mirrors deleteInvitation().
    $is_team_invite = $invitation->isTeamInvite();
    $season_id = $invitation->get('season')->target_id;
    $redirect_team = $is_team_invite ? $invitation->getTeam() : NULL;
    $tournament_id = $redirect_team ? $redirect_team->get('tournament')->target_id : NULL;

    if ($is_team_invite && $tournament_id) {
      $my_reg = $this->entityTypeManager->getStorage('ccsoccer_registration')
        ->loadByProperties([
          'player' => $this->currentUser->id(),
          'tournament' => $tournament_id,
        ]);
    }
    elseif ($season_id) {
      $my_reg = $this->entityTypeManager->getStorage('ccsoccer_registration')
        ->loadByProperties([
          'player' => $this->currentUser->id(),
          'season' => $season_id,
        ]);
    }
    else {
      $my_reg = [];
    }

    if (!empty($my_reg)) {
      $reg = reset($my_reg);
      return $this->redirect('ccsoccer.group', ['registration' => $reg->id()]);
    }

    return $this->redirect('ccsoccer.my_registrations');
  }
```

Notes:
- `$is_tournament`/`$team` from earlier in `nudge()` are scoped inside the 48h else-branch — the throttle path skips them — hence the fresh locals; don't reuse them.
- Variable named `$redirect_team` to avoid colliding with `$team` in the else-branch.
- The `else { $my_reg = []; }` arm covers deleted-team/season edge cases (same as `deleteInvitation()`).

## Deploy & test plan

Code only — no `cim`/`updb`. `drush cr` per environment (opcache/container).

LOCAL regression:
1. Tournament invite, invitee **with** account → Send Reminder → lands on team's group page, "Reminder sent."
2. Tournament invite, email-only invitee (board-allowlisted address → Mailpit) → reminder email arrives, redirect OK.
3. Tournament invite, email-only, **non**-allowlisted → warning "could not be sent", no crash, `notified` NOT stamped.
4. Season invite nudge → unchanged behavior (regression check).
5. Nudge inside 48h window → "Please wait" warning, no crash (this path also crashed before the fix).

## Coordination / PR strategy

- Caleb's invitation-timing PR (Bug 1/2 — Brent/Myk) touches `GroupPane.php`, `OrderCompleteSubscriber::createSeasonRegistration()`, and `removeMember()`/accept flows in `GroupController.php` — different functions, no textual overlap with this fix. Safe as an independent small PR (suggested branch: `fix/tournament_nudge_redirect`); whoever lands second rebases trivially. Only near-adjacency risk: if Caleb also picks up Bug 3 (`deleteInvitation()` restriction, directly below `nudge()`).
- Keep out of this PR: `web/sites/default/default.settings.php` (belongs with Caleb's core-bump commit per SESSION_HANDOFF) and the unrelated `archive/SEASON_TOURNAMENT_DRIFT_AUDIT.md` working-tree edits — but DO update the D14 entry (mark ✅ fixed, note it was fatal not soft-redirect) in the same PR as the code fix.
