# Risk & Safety Model — Onboarding Automation

Concern: we must not break the **11 currently-onboarded warehouses** (live
shipping) while building the registry/automation. This is the committed,
non-negotiable safety approach.

---

## Where the danger actually is

The automation's read-refactor touches code paths that **all 11 live
warehouses** depend on — so a bug there breaks shipping for *everyone*, not
just new yards. Specific failure modes:

| Risk | Why it could break the existing 11 |
|---|---|
| **Config depends on DB at boot** | If `config('gopartsship.warehouses')` is built from a live query, a DB hiccup / mid-migration moment empties it → every warehouse falls back to GA or fails auth. Also breaks `config:cache`. |
| **Backfill transcription errors** | Copying the existing 11 into registry rows could fat-finger a ZIP / location_id / flag → silent wrong behavior for a working yard. |
| **Stale / failed cache** | Cached registry snapshot goes stale or fails to build → dropdowns empty, exclusions wrong. |
| **Semantic edge cases** | `USER-x` webhook entries, the GA fallback in `GoPartsShipService`, the `exclusive` flag — if the registry doesn't reproduce these exactly, behavior shifts subtly. |
| **Hot path** | `ShipmentProcessor` warehouse→id map + the credential lookup run on every live label — the scariest surface. |

---

## The committed design: additive overlay (never migrate the existing 11)

**Do not touch the existing warehouses.** The current hardcoded
config/arrays/exclusions stay exactly as they are and remain authoritative for
the 11. The registry only contributes **additional** entries, merged on top:

```
warehouses   = static_config   +  registry_rows         (static wins on conflict)
b2b_exclude  = hardcoded_ids    ∪  registry b2c_only ids
export menu  = fixed buttons    +  registry vendors
hazmat       = hardcoded ranges ∪  registry hazmat ids
```

Consequences:
- The existing 11 resolve through the **unchanged static path** → they *cannot*
  break from this work.
- The registry is **purely additive** — if it's empty, broken, or its cache
  fails, behavior == today (the static side still answers).
- Only **new yards** (not yet live) flow through the new path, so any bug there
  is contained to things not in production.

This yields ~90% of the automation win (new yards = data, not 11 file edits)
with near-zero blast radius on what's live.

---

## Belt-and-suspenders (required on top of the overlay)

1. **Phase 0 is inert.** Tables + service + backfill + tracking dashboard land
   with **nothing reading them** → safe to merge any day, zero behavior change.
2. **Parity test before any read flips.** A test asserts registry-backed output
   **exactly equals** today's hardcoded output for all 11 warehouses; must diff
   to zero before shipping. Catches backfill typos mechanically.
3. **Keep config static; don't DB-back it at boot.** The merge happens in the
   cached `GpsRegistry` service at call time (static fallback), not in the config
   provider — so `config:cache` and app boot stay independent of the DB.
4. **One read at a time, reversible.** Flip low-risk reads first (dropdown
   names), verify, then the next. Never the credential/auth lookup for existing
   yards. Each is a tiny, separately-revertable commit.
5. **`gps:verify` gate.** Automated check that the existing 11 still appear in
   every dropdown, still auth, still export — run after each step.
6. **No big weekday cutover.** Additive + inert + incremental → there is no
   flag-day; nothing here needs a risky cutover.

---

## Fail loud, never silent (the core guarantee)

"Ship button doesn't work" is the **acceptable** failure mode — loud, obvious,
no data harm, instantly reverted. The real danger is **silent** failures that
ship wrong without anyone noticing. The design must collapse every failure into
the loud bucket.

**Known pre-existing silent footgun:** `GoPartsShipService::getWarehouseCredentials()`
(and the webhook controller's `getWarehouseAuth()`) **fall back to GA's
credentials** when a warehouse code isn't found in config — only a
`Log::warning`. So a missing/typo'd credential → the order is **silently pushed
to the GA account** instead of the yard. That is worse than a dead button: it
mis-ships without anyone noticing.

Guarantees that hold the worst case at "visibly won't ship":

- **G1 — Overlay.** Existing 11 stay on static config; they never move onto a
  registry-sourced credential, so they can't be mis-pointed.
- **G2 — Fail-safe merge.** If the registry read/cache throws or is empty, fall
  back to **static config**, never to an empty list. (An empty warehouse list is
  exactly what would trigger mass GA-fallback — design it out.)
- **G3 — Hard-fail on missing credential.** For registry-managed yards, a
  missing/blank credential must **throw / refuse to ship** (loud), not silently
  use GA. Worth doing regardless of the registry — it converts the one dangerous
  silent path into a loud one.

Other silent modes to keep out of the registry path: wrong `postalCode` → wrong
rates (G3-style validation on sync); HAZMAT can't silently regress because the
overlay **unions** hazmat rules (never replaces the hardcoded ranges).

With G1–G3 the worst case is **"it visibly won't ship,"** never "it silently
shipped to the wrong account."

---

## Residual risk after mitigation

Low, and **bounded to new yards** (not live). The existing 11 keep running on
untouched code. Worst realistic outcome: "a newly-onboarded yard didn't show up
correctly" — caught by `gps:verify` before go-live, affecting nobody currently
shipping.

## Bottom line

- **Migrate the 11 into the registry** → real risk (hot shared paths). Avoid.
- **Overlay** — leave the 11 in code, registry only adds new yards → existing
  warehouses are on unchanged code and effectively cannot be broken by this.

**Committed approach: additive overlay + parity test + inert Phase 0 + no weekday cutover.**
