# Data flow

From Blizzard's edge to Drupal storage, and then out to the UI.

## 1. Authentication

Every API call flows through `BattleNetClient`.

```
BattleNetAuth         BattleNetClient           Blizzard
─────────────         ───────────────           ────────
getAccessToken()  →   (cached per region)
                        │
     pcov / lock   ←   reserveRateSlot()
                        │
                        GET /data/... / /profile/...
                                              →
                                              ← 200 / 304 / 5xx
                      (5xx → retry w/ backoff)
                        │
                      ['data' => ..., 'last_modified' => ...]
```

- Token acquisition: `BattleNetAuth` fetches a client-credentials (app-scoped) token and caches it. This is the default used by `BattleNetClient::get()` when no token is passed.
- User OAuth: `wow_battlenet_login` captures a user token during OIDC login and stashes it in `user.data`. Call sites that need it (currently only `AccountProfileApi` for `/profile/user/wow`) pull it from there and pass it explicitly as the `$accessToken` argument.
- Per-character / per-guild Profile calls use the app token today — no per-request user consent required.
- Rate limit: per-hour + per-second counters in Drupal State, serialized under the `wow:api_rate_limit` lock, enforced across processes.
- Retry: only GETs, only on 5xx, capped at 3 attempts with exponential backoff.
- Conditional: `If-Modified-Since` handled at the client layer — 304 returns `NULL` from `get()`.

## 2. Sync orchestration

Three sync shapes, all living in `src/Sync/`:

### Catalog sync (taxonomy-backed reference data)

```
TaxonomySyncBase::sync($region)
  ↓
fetchIndex → state[wow.last_modified.X.region] → If-Modified-Since
  ↓
for each item: findOrCreate(wow_blizzard_id)
  ↓
mapFields() → $term->save()
  ↓
update state[wow.last_modified.X.region]
```

Subclasses only implement `mapFields()`. Result: created / updated / skipped counts.

### Catalog sync (queue-backed content entities)

Used when the entity set is large (~9,000 achievements). Instead of a single long sync:

```
enqueue(region)
  ↓
fetchConditional → for each item:
  Queue::enqueueJob(new Job('wow_X_sync', [blizzard_id, region]))
  ↓
(later, out-of-band) queue worker runs syncSingle(region, blizzard_id)
  ↓
api->get{Type}(id) → syncEntity() → $entity->save()
```

Queue workers run via drush cron or a dedicated runner.

### Profile sync (per-character / per-guild)

```
CharacterSync::lookup(region, realm, name)
  ↓
/profile/wow/character/{realm}/{name}  → Character entity
  ↓
/profile/wow/character/{realm}/{name}/character-media → avatar + inset
  ↓
invokeDataPlugins()
  ├─ AchievementData  → /achievements         → wow_achievement_progress rows
  ├─ MountData        → /collections/mounts   → wow_mount_collection rows
  ├─ PetData          → /collections/pets     → wow_collected_pet rows
  ├─ TitleData        → /titles               → wow_title_award rows
  ├─ ToyData          → /collections/toys     → wow_toy_collection rows
  └─ ReputationData   → /reputations          → wow_reputation_standing rows
```

Each provider is isolated: one throwing doesn't block the others, and the base character still saves.

Guild sync mirrors this through `GuildSync` + `GuildDataProvider` plugins (only an interface ships — no implementations at present, but the extension point is wired and tested).

## 3. Storage layout

```
Reference data       → taxonomy_term  (vid = wow_*)
                       + wow_blizzard_id, wow_last_fetched

Catalogs             → wow_{achievement,mount,pet,...}
                       + last_fetched (base field)

Per-owner data       → wow_{entity}_collection / _progress / _award / _standing
                       + owner columns projected in by owner modules
                       + last_fetched (base field)

Characters / Guilds  → wow_character, wow_guild
                       + uid / is_main (when wow_user is enabled)
```

## 4. TTL sweep

Daily cron job (`wow_cron → wow.ttl_sweeper → sweep()`) scans every content entity type with a `last_fetched` field and deletes rows older than `DATA_TTL_DAYS` (30). Taxonomy reference data is kept fresh by the scheduled sync commands, not the sweeper. See [lifecycle](lifecycle.md).

## 5. UI output

The suite ships no Views, no field formatters, no custom themes. UI is left to each site:

- Views can query every entity directly.
- The character canonical page (`/wow/character/{region}/{realm}/{name}`) ships a `CharacterPageRenderer` that collects `CharacterPageSection` value objects from `CharacterDataProvider` plugins via `buildPageSections()`. Sections are organized by named slots (`TITLE_RIBBON`, `COLLECTIONS`, `ACHIEVEMENTS`, `EQUIPMENT`, `GENERIC`).
- The dashboard at `/admin/reports/wow` is the only admin-facing UI the suite renders itself.
