# Flagged Orders — Admin Surfacing (Proposal, Deferred)

**Date:** July 3, 2026
**Status:** DEFERRED — documented for future use, not being built now.
**Decision:** At current scale (~200 players/season), a player who pays but doesn't
receive their product will email the league directly. That self-reporting is an
adequate backstop, and every flag is already written to the watchdog log
(`dblog`) at error level, so the failures are discoverable. Revisit if volume grows.

---

## The problem

Several checkout edge cases result in an order that has **collected payment** but
where the expected outcome (a registration, a team, or a fully-credited total) did
**not** happen. In each case the code logs the problem and tags the order with a
data flag, then returns — but nothing proactively surfaces these to an admin.
Today they're only visible by reading the watchdog log or opening individual orders.

Four of the five are silent *financial* failures: money was taken, no artifact
was created. That's the case worth surfacing.

## The five flags

All are written via `$order->setData($key, $detail)` in
`web/modules/custom/ccsoccer/src/EventSubscriber/OrderCompleteSubscriber.php`:

| Flag key | Set at (line) | Meaning | Money taken? |
|----------|---------------|---------|--------------|
| `ccsoccer_credit_shortfall` | ~285 | Order total was discounted by more credits than were actually available to deduct at placement. League absorbs the gap unless caught. | Yes (under-collected) |
| `ccsoccer_season_registration_failed` (`reason: season_inactive`) | ~373 | Season toggled inactive/hidden mid-checkout; no registration created. | Yes |
| `ccsoccer_season_registration_failed` (`reason: capacity_exceeded`) | ~402 | Season filled before this order completed; no registration created. | Yes |
| `ccsoccer_team_name_collision` | ~553 | Duplicate tournament team name at checkout; team + registration not created. Deposit collected. | Yes (deposit) |
| `ccsoccer_tournament_full` | ~580 | Tournament at `max_teams` when a team-create order completed; team + registration not created. Deposit collected. | Yes (deposit) |
| `ccsoccer_team_capacity_exceeded` | ~842 | Player joined a tournament team that filled during the race window. | Varies |

Each flag's detail payload includes the relevant IDs, counts, and a
`detected_at` timestamp (see the `setData` calls for exact shapes).

## Key technical constraint

These flags live in the serialized `commerce_order.data` blob column. That column
**cannot be filtered by a Drupal View or an entity query**. So any surfacing UI
must load orders and decode the data in PHP — a plain View is not an option
without first denormalizing the flags into a real field or a side table.

This is not a problem in practice: every existing report in the module
(`ReportController::tournamentDeposits`, city payment, insurance) is already a
custom controller rendering a table. A flagged-orders report would follow the
same pattern.

---

## Proposed solution (when/if built)

Recommended package, in order of value:

### 1. Centralized flag helper + immediate notification
Replace the five scattered `setData()` blocks with one helper —
`flagOrderForReview($order, $type, $detail)` — that (a) stores the structured
detail as today and (b) immediately emails a board/admin address via the existing
`NotificationService`. This is the highest-value piece: within minutes of a
paid-but-failed order, someone is told, rather than discovering it later. It also
tidies the subscriber into one consistent flagging path.

Suggested recipient: `ccsoccer@ccsoccer.com` (the shared inbox the board already
monitors).

### 2. Report page
`/admin/ccsoccer/reports/flagged-orders`, linked from the Reports landing. Scans
recent orders (bounded window, e.g. last few months, or all non-cancelled —
volume is tiny), decodes the five flag keys, and renders a table grouped by flag
type: order #, date, customer, flag + human-readable detail, amount paid, and
action links (**View order** → the Commerce order page where refunds are issued).

### 3. "Resolved" mechanism
A `ccsoccer_flag_resolved` order-data key set by a **Mark resolved** action, which
the report filters out so handled orders drop off the list. No new entity or DB
schema required.

### Access control
Other reports gate on `view reports`, but the July 3 security review flagged that
permission as over-broad (it already exposes every player's credit ledger). Since
this report shows payment/refund data, gate it on something tighter —
`manage registrations` or `administer ccsoccer` — rather than widening
`view reports` further.

### Explicitly out of scope
A dedicated flagged-order entity with native Views integration and an
assignment/audit workflow. That's the enterprise version — far more code, not
justified at this scale. The data-key approach above delivers ~90% of the value.

---

## Interim mitigation (already in place)

- Every flag is logged at **error** level to `dblog`, e.g. "Credit shortfall on
  order @oid…", "CAPACITY EXCEEDED…", "Tournament @tid was at max_teams…". These
  are visible at `/admin/reports/dblog` (filter by the `ccsoccer` channel).
- The idempotency guard (Finding B) ensures a flagged order is marked
  `ccsoccer_completion_processed`, so it will not be reprocessed — an admin
  resolves it manually (refund / manual registration / waitlist) without risk of
  double-charging or double-crediting.
- Players who pay without receiving a product will self-report by email.

## Effort estimate (if picked up later)
- Notification helper only: ~1–2 hours.
- + Report page: ~half a day.
- + Mark-resolved: ~1–2 hours more.
