fix(formation): name-search returns null (not false) on adapter error

E2E harness exposed that the NV name-search adapter times out on a stale input
selector and search_name() swallowed the error and returned available=False --
i.e. it would tell customers an AVAILABLE name is taken. Now returns
available=None ('could not determine') on adapter error / unknown state, which the
API already maps to null. The NV/TX portal selectors still need a scraping fix
(separate task; e2e harness is the acceptance test) before enabling a self-serve
formation checkout. Documented full e2e results + the bugs caught (missing ERPNext
Items, entity_type case, missing /name-search route) in the readiness doc.
This commit is contained in:
justin 2026-06-09 08:06:43 -05:00
parent 4c0decd175
commit 76c4d55603
3 changed files with 53 additions and 5 deletions

View file

@ -5,8 +5,13 @@ incorporation orders end-to-end (name search, accept into ERPNext, etc.), and th
mechanics of moving a corp out of Delaware + annual report + EIN.
## Short answer
- **New NV/TX formation orders:** mostly built, but **NOT yet verified e2e** and the
DEXIT page does not point at this checkout (it points at a contact form).
- **New NV/TX formation orders - order intake + ERPNext now VERIFIED e2e, BUT live
name search is BROKEN.** The 2026-06-09 e2e harness (`scripts/e2e-formation-order.mjs`)
confirmed: live order insert -> ERPNext customer -> Sales Order with the correct
`BUSINESS-FORMATION` + `STATE-FILING-FEE` line items and totals ($254 NV LLC,
$479 TX corp) -> DB linkage, all PASS for both NV and TX. **However the name-search
adapters silently fail** (see below), so we cannot yet tell a customer whether their
chosen name is available. The DEXIT page points at a contact form, not this checkout.
- **"Move a company" (DE -> NV/TX conversion/domestication):** **NOT built.** There
is no order type, no SKU, and no fulfillment for a conversion. This is the core of
the DEXIT promise and is the biggest gap.
@ -19,6 +24,46 @@ mechanics of moving a corp out of Delaware + annual report + EIN.
So: **do not turn on a "buy now" DEXIT checkout yet.** Keep the page as a lead-gen
"get my estimate" CTA (which it currently is) until the flow below is built + tested.
## E2E test results (2026-06-09) and the name-search bug
Ran `scripts/e2e-formation-order.mjs` inside the api container against live prod
(real DB + real ERPNext, no real Stripe charge, no real state filing).
**PASS - order intake + ERPNext Sales Order (both NV and TX):**
- live formation_orders insert,
- ERPNext customer find/create,
- Sales Order created with `BUSINESS-FORMATION` + `STATE-FILING-FEE` line items,
correct totals ($254 NV LLC = $179 + $75; $479 TX corp = $179 + $300),
- `formation_orders.erpnext_sales_order` linkage written,
- cleanup (cancel+delete SO, delete order row).
Bugs the harness caught and we fixed:
- The formation Sales Order referenced ERPNext Items `BUSINESS-FORMATION` and
`STATE-FILING-FEE` (and `FOREIGN-QUAL-SINGLE/MULTI`) that **did not exist** -> the
SO creation was silently failing on every formation order. Created the Items.
- `entity_type` check constraint requires lowercase (`llc`, `corporation`, ...).
- The API's `GET /states/:code/name-search` called `WORKER_URL/name-search`, but the
worker had **no such route** (404 -> silent fallback to stale `entity_cache`).
Added a synchronous `/name-search` worker route, and fixed `handle_name_search`
(both referenced a nonexistent `search_name_sync`).
**FAIL (open) - live name search silently returns "unavailable" for everything.**
After wiring the route, a deliberately unique nonsense name still returns
`available=false` with no similar names. Root cause: the **NV state-portal adapter
times out** (`Page.fill: Timeout ... waiting for locator("input[type=text], ...")`)
because the Nevada SOS / SilverFlume search page no longer matches the adapter's
input selector, and `search_name()` swallows the exception and **defaults to
`available=False`**. So:
- We would wrongly tell customers an available name is taken (or never validate it).
- This is a **portal-scraping maintenance task**: inspect the current NV (and verify
TX) name-search DOM, update `states/nv/adapter.py` (and `tx`) selectors/flow, and
make `search_name()` distinguish a real "taken" result from an adapter error
(return an explicit error/unknown state, never a false "taken"). The harness
re-run is the acceptance test.
**Conclusion:** the ERPNext/order plumbing is sound and verified; **do not enable a
self-serve formation checkout until name search is fixed** (a customer must be able
to trust the availability check), and the "move a company" flow is still unbuilt.
## What already exists (the good news)
The corporate/formation machinery is real and reusable:
- **Checkout + order intake:** `api/src/routes/checkout.ts` has `order_type: "formation"`,

View file

@ -68,7 +68,7 @@ class FilingStatus(str, Enum):
@dataclass
class NameSearchResult:
available: bool
available: bool | None # True/False, or None when the lookup could not be completed (adapter error)
exact_match: bool = False
similar_names: list[str] = field(default_factory=list)
state_code: str = ""

View file

@ -40,7 +40,7 @@ async def search_name(name: str, state_code: str) -> NameSearchResult:
code = state_code.upper()
if code not in STATES:
return NameSearchResult(
available=False,
available=None,
searched_name=name,
state_code=code,
raw_response=f"Unknown state code: {code}",
@ -58,8 +58,11 @@ async def search_name(name: str, state_code: str) -> NameSearchResult:
return result
except Exception as exc:
LOG.error("Name search failed in %s: %s", code, exc, exc_info=True)
# available=None => "could not determine" (adapter error/portal change).
# NEVER return False here: a False would be read as "name taken" and could
# block a legitimate order or mislead the customer.
return NameSearchResult(
available=False,
available=None,
searched_name=name,
state_code=code,
raw_response=f"Error: {exc}",