# Go-Parts Ship Onboarding — Automation Design

Goal: onboard **many yards** to GPS with a consistent, idempotent, trackable
process. Inputs arrive in stages (some upfront, some only after the vendor
replies). We must always know **what's done where**.

---

## 0. The core problem (why naive scripting won't scale)

Today, onboarding one yard requires hand-editing ~11 code locations:

| # | Location | What | Scales? |
|---|----------|------|---------|
| 1 | `config/gopartsship.php` | warehouse entries (cred, zip, exclusive) + `USER-x` webhook entry | hand-edit |
| 2 | `WarehouseHelper` `$warehouseNames`/`$warehousePostalCodes` | dropdown names + zips | hand-edit |
| 3 | `ShipmentController::getWarehouses()` | legacy ship-form list | hand-edit |
| 4 | `supplier_external_ship_locations` | supplier→GPS location id | migration |
| 5 | GPS `webhook_configurations` | SHIP_NOTIFY row | GPS side |
| 6 | `suppliers.can_autofulfill` | B2C dropdown visibility | data |
| 7 | `OldFulfillmentResource::getB2BSuppliers()` | B2B exclusion | hand-edit |
| 8 | `OrderItem::isHazmat()` + `routes/web.php` payload | HAZMAT supplier ranges | hand-edit |
| 9 | `FulfillmentOrder` CSV methods + `ListOldFulfillments` actions + blade | per-vendor export button | hand-edit |
| 10 | `UsedPartsShipInfoResolver` | supplier→recycler_id map | hand-edit |
| 11 | `ShipmentProcessorService`/`ShipmentActionBulk` warehouse→id maps | hand-edit |

**Hand-editing 11 files per yard × hundreds of yards = unworkable and inconsistent.**
Hardcoded per-vendor export *buttons* literally explode the UI.

**Principle:** make every one of these read from a **single source of truth**
table, then onboarding becomes *data entry + a sync command*, never code edits.

---

## 1. Architecture: registry + read-refactor + staged pipeline

Three layers:

1. **Registry** (Central DB) — single source of truth for every GPS account,
   warehouse/location, and sub-user, plus the per-yard **policy flags**
   (exclusive, b2c_only, hazmat_mode, export_format, recycler_id) and
   **onboarding status**.
2. **Read-refactor** — the 11 integration points above read the registry
   (via one `GpsRegistry` service, cached) instead of hardcoded arrays. This is
   one-time engineering (Phase 0). After it, new yards need **zero** code edits.
3. **Pipeline** — idempotent, staged commands that fill the registry, provision
   GPS, sync Central, and verify — each writing **status** so we always know
   what's done where.

Key automation primitive discovered this session: **almost every GPS-side fact
is derivable from the `shipdev` DB by email + ZIP** (that's exactly how we found
USER-47 and locations 37–42). So the registry can self-populate from shipdev
rather than relying on hand-copied credentials/IDs.

---

## 2. Data model (Central)

```
gps_accounts            -- one per vendor GPS account
  id
  yard_name
  account_email                 -- vendor login email (input)
  gps_user_id                   -- shipdev users.id        (resolved from shipdev by email)
  account_code                  -- 'USER-<gps_user_id>'    (resolved)
  authorization                 -- 'Basic ...'             (resolved from shipdev, not hand-copied)
  exclusive            bool      -- GPS-only vs dual        (policy input)
  webhook_configured   bool      -- SHIP_NOTIFY present in shipdev (verified)
  status               enum      -- see §4
  ...timestamps, operator

gps_warehouses          -- one per location / Central supplier
  id
  account_id            FK gps_accounts
  supplier_id           FK suppliers (unique)   -- already created per warehouse (input)
  warehouse_code        -- e.g. WAM-D           (input)
  display_name          -- 'Wheels America - Dallas TX' (input/derived)
  postal_code, city, state                       (input)
  gps_location_id       -- shipdev ship_from_locations.id (resolved by user_id+zip)
  b2c_only             bool      -- block from B2B          (policy input)
  hazmat_mode          enum(none|hollander|description)     (policy input)
  export_format        enum(manager|counselman|partsbox|...) (policy input)
  recycler_id          int null  -- for ship extras / buy price (policy input)
  active               bool
  status               enum

gps_sub_users           -- vendor staff (Stage after vendor replies)
  id
  account_id            FK
  email, name                                   (vendor input)
  gps_user_id           -- shipdev users.id (sub-user)  (resolved)
  assignment            enum(all|specific|none)         (vendor decision input)
  -- specific → gps_sub_user_location rows (warehouse_id list)

gps_onboarding_events   -- append-only audit: who did what stage when (optional but recommended)
```

Existing tables: `suppliers`, `supplier_external_ship_locations` become
**derived/written by sync** from `gps_warehouses` (single source of truth).

---

## 3. Inputs required, by stage (the heart of the request)

Source legend: **OPS** = operator/spreadsheet · **VENDOR** = yard replies ·
**AUTO** = resolved from shipdev/Central, no human input.

**Precondition (gate) — supplier exists first.** Onboarding assumes the supplier
already exists in `suppliers`. Stage 0 is **hard-gated**: every location must map
to a finalised, existing `supplier_id` (+ code) before the registry row is
created or any later stage runs. `gps:import` validates supplier existence and
rejects unmapped rows; the GPS account may exist independently, but our flow
will not advance without the supplier mapping. (`gps_warehouses.supplier_id` is
the FK everything else keys off.)

| Stage | Trigger | Inputs (source) | Acts on | Produces |
|-------|---------|-----------------|---------|----------|
| **0 Intake** | batch of yards ready | per location: supplier_id, warehouse_code, city/state/zip (OPS); per yard: account_email, exclusive, b2c_only, export_format, hazmat_mode, recycler_id? (OPS) | Central registry | rows @ `intake` |
| **1 GPS account + playground** | expose a yard early (before feed import) | account_email (OPS, from intake) | GPS: create account + cred, send access | gps_user_id, account_code, authorization → **AUTO** back-filled to registry |
| **1b Sample orders** | give vendor something to ship | account (AUTO) + ≥1 location (AUTO) | GPS: seed test orders (Orders/CreateOrder API or shipdev insert) | playground orders (flagged test, cleaned up later) |
| **2 Locations** | locations confirmed | city/state/zip (OPS, from intake) | GPS: create ship_from_locations | gps_location_id per warehouse → **AUTO** back-filled |
| **3 Webhook** | account live | account_code (AUTO) | GPS: SHIP_NOTIFY webhook row | webhook_configured=true (**AUTO** verified) |
| **4 Central sync** | registry complete for yard | registry (AUTO) | Central: `gps:sync` — set can_autofulfill, write supplier_external_ship_locations, warm caches; all 11 points read registry | yard live in all dropdowns/exports/hazmat |
| **5 Sub-users** | vendor replies w/ staff | sub_user_email, name, assignment=all\|codes\|none (VENDOR) | GPS: create sub-users + sub_user_locations | sub-users provisioned (**AUTO** verified) |
| **6 Verify** | any stage transition | none (AUTO) | Central+GPS read checks | verification report, status→`live` |

**True manual inputs total:** intake row data + 5 policy flags per yard + sub-user
assignment decisions. Everything else is auto-derived. That's the consistency win.

---

## 4. Status model (what's done where)

Per-account and per-warehouse `status` advances through:

```
intake → account_created → playground_seeded → locations_created
       → webhook_configured → central_synced → verified → live
```
Sub-users tracked separately (`pending → provisioned`), since they arrive late
and in parallel.

**Self-healing:** a `gps:reconcile` command re-derives *actual* state from
shipdev + Central and corrects `status` — so the tracker can never silently lie.
Status is computed truth, not a manually-flipped flag.

**Visibility:** a Filament admin page "GPS Onboarding" = a grid of yards ×
stages with status chips, blockers, and the next required input. This is the
"track what's done where" surface for ops.

---

## 5. Tooling (idempotent commands)

All upsert-based, re-runnable, keyed on (account_email) / (supplier_id):

- `gps:import {csv}` — Stage 0 bulk intake (one row per location). Validates,
  upserts registry. Second CSV/sheet for sub-users.
- `gps:resolve {yard?}` — Stage 1/2 readback: query shipdev by email → account
  (user_id, code, authorization); by (user_id, zip) → location_id. Fills registry.
- `gps:seed-samples {yard} {count}` — Stage 1b sample orders via the existing
  `GoPartsShipService::createOrder` (with test flag).
- `gps:provision {yard}` — Phase 2 only: write GPS side (account/locations/
  sub-users/webhook) — see §7.
- `gps:sync {yard?}` — Stage 4 Central activation (idempotent; safe cache warm).
- `gps:verify {yard?}` — Stage 6 automated checks (dropdowns, B2C shows / B2B
  blocks, hazmat blocks, webhook row exists, sample order round-trips, location
  mapping present). Outputs pass/fail per check.
- `gps:reconcile` — drift detection, status correction.

---

## 6. Read-refactor surface (Phase 0 engineering, once)

Introduce `App\Services\Gps\GpsRegistry` (cached). Refactor each point to read it:

- config warehouses → `GpsRegistry::warehouses()` merged into `config('gopartsship')`
- `WarehouseHelper` names/zips → registry
- `ShipmentController::getWarehouses()` → registry (+ legacy statics)
- `getB2BSuppliers()`/`getB2CSuppliers()` exclusions → `where b2c_only`
- `OrderItem::isHazmat()` + web.php payload → registry hazmat_mode + supplier ids
- export buttons → **render dynamically** from active accounts; pick CSV template
  by `export_format`. (Critical: with many yards, replace fixed buttons with a
  searchable "Export for vendor…" picker.)
- `UsedPartsShipInfoResolver` recycler map → registry
- `ShipmentProcessor`/`ActionBulk` warehouse→id maps → registry

Backfill the registry from current `config/gopartsship.php` + shipdev so existing
yards (PAMS, Counselman, PartsBox, WAM, Spalding, OEM) are represented day one,
then flip each read to the registry behind a fallback.

---

## 7. shipdev (GPS-side) interaction strategy — phased

- **READ is safe today** (we already query shipdev; it IS prod). Phase 1 relies
  only on reads (`gps:resolve`/`verify`).
- **WRITE (account/location/sub-user/webhook/sample-order creation)** couples
  Central to GPS's schema. Two options, needs GPS-team decision:
  - (a) GPS exposes provisioning **API endpoints** (cleanest), or
  - (b) Central writes shipdev via a dedicated connection, bounded + idempotent.
- **Phasing:** Phase 1 keeps GPS account/location creation in the GPS UI (as ops
  does today) and automates readback + Central + samples. Phase 2 automates GPS
  provisioning once (a)/(b) is agreed.

---

## 8. Rollout

1. **Phase 0** (eng): registry tables + `GpsRegistry` + read-refactor + backfill
   existing yards + `gps:verify` + Filament tracking page. *No behavior change.*
2. **Phase 1** (ops, per batch): `gps:import` → (ops creates GPS acct/locations) →
   `gps:resolve` → `gps:seed-samples` → `gps:sync` → `gps:verify`. Hundreds of
   yards with zero code edits.
3. **Phase 2**: automate GPS-side provisioning (§7) end-to-end.

---

## 9. Open decisions / risks

- GPS-side writes: API vs direct shipdev writes vs stay-manual (§7) — **needs GPS team**.
- Credential storage in Central registry (encrypt `authorization`; or fetch
  from shipdev at runtime and never store).
- UI scale: hundreds of warehouses in ship dropdowns → need grouping/search;
  exports → vendor picker not buttons.
- Policy defaults: are GPS used-parts yards *always* exclusive + b2c_only +
  description-hazmat? If yes, fewer manual inputs (derive from a "yard type").
- Supplier creation: assumed pre-done; could be folded into `gps:import`.
- Sample-order cleanup policy + test-data flagging in shipdev.
```
```

---
_Companion to [01-onboarding-checklist.md](01-onboarding-checklist.md) and [03-risk-and-safety-model.md](03-risk-and-safety-model.md). Reference yards onboarded this session: PAMS, Counselman, PartsBox, Wheels America (USER-47), Spalding (USER-49), OEM Garages (USER-48)._
