AlkantarClanX12
| Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/docs/ |
| Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/docs/lead-intake-migration.md |
# Lead Intake Migration — Legacy → Leadbox Platform (LBT-1468)
How to cut a dealer's Ninja Forms lead submissions over from the legacy backend
(`leadbox-be`, `POST /api/v1/lead`) to **Leadbox Platform** (`POST /api/v1/leads`),
without a deploy, using the runtime routing switch shipped in this plugin.
The switch has three modes, controlled per-site:
| Mode | Behavior |
|------|----------|
| `legacy` *(default)* | Leads go only to the legacy backend. Byte-for-byte identical to pre-LBT-1468 behavior. |
| `both` | **Dual-write.** Legacy is authoritative (drives success/retry); Leadbox Platform receives a copy, fired non-blocking. Used to validate parity before cut-over. |
| `leadbox_os` | Leads go only to Leadbox Platform. Full cut-over. |
---
## How the switch is resolved
The effective target is resolved in this order (first match wins):
1. **`LEADBOX_LEAD_INTAKE_TARGET` PHP constant** in `wp-config.php` — for ops-driven
cut-over without DB access. An invalid value is ignored.
2. **`lead_intake_target` option** — set via WP admin or WP-CLI.
3. Default: **`legacy`**.
> When the constant is defined it **overrides** the option. The admin UI disables
> the dropdown and shows a notice, and `wp leadbox lead-intake-target get|set`
> warns that the option is being overridden.
---
## Prerequisites (per dealer)
Before flipping a dealer to `both`/`leadbox_os`, get from the Leadbox Platform side:
1. **A website lead-intake API key** (`wl_key_…`). Issued by Leadbox Platform via:
- Artisan: `sail artisan leads:website-key:generate {tenant_id} --location={id} --name="..."`, or
- Admin API: `POST /api/leads/api-keys` (`{ location_id, tenant_id, name }`).
2. **Location binding decision.** Prefer a **location-scoped key** (key bound to one
location). Then the plugin does **not** need to send `LocationId` — Leadbox Platform
resolves the location from the key. The mapper only sends `LocationId` if a form
explicitly provides a numeric one (multi-location sites).
> **Base URL is configurable, with a default.** Leave the **Leadbox Platform
> Lead Intake URL** field blank to use the default
> (`LB_API_Settings::LEADBOX_OS_LEAD_INTAKE_URL` = `https://os.leadboxhq.com`).
> Set it only to point a site at a different Platform host (e.g. staging). The
> value is normalised (scheme forced to `https`, trailing slash stripped); an
> invalid URL is ignored and the default is used. The client appends `/api/v1/leads`.
Configure these in **WP Admin → Leadbox settings → "Backend Connection"**:
- **Lead Intake Target** → the mode (or use WP-CLI). The URL/token fields below
are hidden while this is `legacy`, since they don't apply.
- **Leadbox Platform Lead Intake URL** → optional override; blank = default host.
- **Leadbox Platform Lead Intake Token** → the `wl_key_…`.
---
## Step 1 — Deploy the plugin update (zero behavior change)
Ship the plugin with `lead_intake_target = legacy` everywhere. Because `legacy` is
the default and delegates 1:1 to the existing legacy client, there is **no behavior
change** for any site until a dealer is explicitly flipped.
Verify a dealer is in legacy mode:
```bash
wp leadbox lead-intake-target get
# Effective target : legacy
```
---
## Step 2 — Enable dual-write (`both`) and monitor parity ≥ 24h
For a migrated dealer:
1. Set the URL + token (above).
2. Flip to `both`:
```bash
wp leadbox lead-intake-target set both
```
In `both` mode:
- The **legacy POST stays authoritative** — it drives the form's success path and
the failed-lead retry queue. Leadbox Platform receives a copy fired **non-blocking**, so
a Leadbox Platform error can never block, fail, or delay the legacy submission, and never
enqueues a retry.
Monitor for **at least 24 hours** and compare legacy vs. Leadbox Platform:
- **Counts** — leads landing in legacy vs. created in Leadbox Platform (per hour).
- **Field parity** — name/email/phone/message/source/vehicle resolve correctly in OS.
- **Error rate** — Leadbox Platform rejections (see *Observability* below).
> Because OS has no idempotency mechanism, every POST creates a new conversation/deal
> in OS (contact is deduped by email/phone, conversation is not). In `both` mode this
> is fine — OS is a shadow copy. Do not retry OS failures in `both` mode (the plugin
> doesn't).
---
## Step 3 — Cut over (`leadbox_os`)
Once parity is confirmed:
```bash
wp leadbox lead-intake-target set leadbox_os
```
Now leads go **only** to Leadbox Platform. The legacy-only Ninja Forms action toggles
(`Use Backend Autoresponder?`, `Send ADF To`) are hidden in the form action editor;
the skip flags (ADF / Shift Digital / Delivr) remain because Leadbox Platform honors them.
Validate: submit a test lead and confirm it appears in Leadbox Platform and **not** in the
legacy backend.
> A failed OS POST in `leadbox_os` mode IS authoritative — it enqueues to the
> `{prefix}leadbox_leads` retry table and `retryFailedLeads()` re-sends it to OS.
---
## Rollback
Rollback needs **no deploy** — just flip the target back:
```bash
wp leadbox lead-intake-target set both # or: legacy
```
For an emergency, ops can hard-pin the target in `wp-config.php` (overrides the
option and disables the admin control):
```php
define('LEADBOX_LEAD_INTAKE_TARGET', 'legacy');
```
---
## WP-CLI quick reference
```bash
# Show effective target, stored option, and any constant override
wp leadbox lead-intake-target get
# Set the stored option (validated: legacy | leadbox_os | both)
wp leadbox lead-intake-target set both
wp leadbox lead-intake-target set leadbox_os
wp leadbox lead-intake-target set legacy
```
---
## Observability
Every dispatch is logged via `leadbox_logger()`:
- **Success** → `info` level (message `lead_dispatch`), with `target`, `backend`,
`form_id`, `success`, `error_code`.
- **Failure** → `error` level (`lead_dispatch_failed`), plus the OS client logs the
HTTP status, latency, and error.
> The logger's default minimum level is `ERROR` (production-safe), so routine `info`
> dispatch logs are suppressed unless `LEADBOX_LOG_LEVEL` is lowered in `wp-config.php`
> (e.g. `define('LEADBOX_LOG_LEVEL', 'info');`). Failures are always recorded.
> When `WP_DEBUG_LOG` is on, entries also land in `wp-content/leadbox-debug.log`.
### Dispatch log (`wp-content/leadbox-lead-dispatch.log`)
For the `both` parity window there is a **dedicated text log**, separate from the
PSR-3 logger and written **always** (independent of `WP_DEBUG_LOG` / log level).
It records one line per dispatch **only when `lead_intake_target` is `both` or
`leadbox_os`** — default-`legacy` sites write nothing.
Each line tells you whether the send succeeded and **why**, with PII partially
masked (`j***@gmail.com`, `***1234`):
```
[2026-06-05 14:23:01] OK both/leadbox_os form=12 sub=4567 http=200 lat=312ms lead_id=cnv_abc contact=j***@gmail.com phone=***1234
[2026-06-05 14:23:05] FAIL both/leadbox_os form=12 http=422 err=validation_failed lat=240ms contact=j***@gmail.com phone=***1234 msg="Email or Phone required"
```
Read it from the terminal:
```bash
wp leadbox lead-log tail # last 50 lines
wp leadbox lead-log tail --lines=200 --failures # last 200, failures only
# or plain grep:
grep ' FAIL ' wp-content/leadbox-lead-dispatch.log
```
> **`both` mode captures the real platform outcome without delaying the visitor.**
> The platform copy is dispatched **after** the HTTP response is sent
> (`fastcgi_finish_request()`), then read in blocking mode to log the true status.
> This requires **PHP-FPM** (LocalWP and most hosts use it). Without FPM it falls
> back to fire-and-forget and the line is tagged `note=sent_no_confirmation`
> (the lead is still sent; only the status read is skipped to avoid blocking).
>
> Disable the log entirely with `define('LEADBOX_LEAD_DISPATCH_LOG', false);`.
> Relocate it with the `leadbox_dispatch_log_path` filter.
### Leadbox Platform error codes (from `LB_OS_API_Client`)
| `error_code` | HTTP | Meaning |
|--------------|------|---------|
| `os_not_configured` | — | `wl_key_` token missing in settings |
| `network_error` | — | Connection/timeout/DNS failure |
| `spam_rejected` | 400 | Caught by the OS spam filter |
| `auth_failed` | 401 | Invalid/revoked/missing `wl_key_` |
| `location_mismatch` | 403 | Payload `LocationId` ≠ the key's bound location |
| `validation_failed` | 422 | Field validation (e.g. missing Email and Phone) |
| `location_unresolved` | 422 | Tenant has no location to receive the lead |
| `rate_limited` | 429 | Over the per-key rate limit (300/min) |
| `server_error` | 5xx | Leadbox Platform internal error |
---
## Vehicle resolution (stock enrichment)
The Platform resolves the vehicle of interest **exactly**, by either:
1. `VehicleId` — but this is the **website's** numeric vehicle id, which does **not**
match the Platform's own inventory ids; it only resolves where the two id spaces
happen to coincide. A miss returns `warnings: ["vehicle_not_found"]` (HTTP 200) and
no vehicle is linked.
2. `AdditionalData.stock` — the **stock number**, which is stable across systems.
So before dispatching, `LB_OS_API_Client` runs a **best-effort enrichment**: when the
lead has no stock, it determines the vehicle id and looks the vehicle up via the
inventory API (`LB_API_Client::VehicleDetail`), then adds its `stocknumber` to the lead.
The mapper forwards it as `AdditionalData.stock`, so the Platform resolves the vehicle
by stock regardless of id-space mismatches. The lookup is cached, never throws, and
never blocks the dispatch — a failure just sends the lead without a resolved stock.
The vehicle id is taken from, in order:
1. the form's `vehicleid` field — populated from the `?vehicle_id=` query string on the
dedicated lead-form pages (`/form/...?vehicle_id=167`); then
2. a fallback parsed from the `referrer` (the page URL the plugin always sends): forms
**embedded on a VDP** (clean permalink, no query string) submit an empty `vehicleid`,
but the VDP slug ends in `-{id}` (see `get_vdp_slug()`), so the id is recovered from
the URL.
> No form change is required. Sites whose forms already supply a `stocknumber`/`stock`
> field skip the lookup entirely.
## Notes / limitations
- **No autoresponder in Leadbox Platform.** The legacy `backend-autoresponder` toggle is
removed from the action UI in `leadbox_os` mode and has no OS equivalent.
- **No ADF recipient list.** `SendADFTo` is hidden in `leadbox_os` mode and the mapper
never forwards it.
- **Skip flags are preserved.** `SkipADF`, `SkipShiftDigital`, `SkipDelivr` are valid
in Leadbox Platform (they suppress ADF/Serti/Cochrane, ShiftDigital, FordDirect
respectively) and are forwarded as real booleans.
- **Timeout** is 5s, matching the legacy client.