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:
parent
4c0decd175
commit
76c4d55603
3 changed files with 53 additions and 5 deletions
|
|
@ -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.
|
mechanics of moving a corp out of Delaware + annual report + EIN.
|
||||||
|
|
||||||
## Short answer
|
## Short answer
|
||||||
- **New NV/TX formation orders:** mostly built, but **NOT yet verified e2e** and the
|
- **New NV/TX formation orders - order intake + ERPNext now VERIFIED e2e, BUT live
|
||||||
DEXIT page does not point at this checkout (it points at a contact form).
|
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
|
- **"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
|
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.
|
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
|
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.
|
"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)
|
## What already exists (the good news)
|
||||||
The corporate/formation machinery is real and reusable:
|
The corporate/formation machinery is real and reusable:
|
||||||
- **Checkout + order intake:** `api/src/routes/checkout.ts` has `order_type: "formation"`,
|
- **Checkout + order intake:** `api/src/routes/checkout.ts` has `order_type: "formation"`,
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class FilingStatus(str, Enum):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NameSearchResult:
|
class NameSearchResult:
|
||||||
available: bool
|
available: bool | None # True/False, or None when the lookup could not be completed (adapter error)
|
||||||
exact_match: bool = False
|
exact_match: bool = False
|
||||||
similar_names: list[str] = field(default_factory=list)
|
similar_names: list[str] = field(default_factory=list)
|
||||||
state_code: str = ""
|
state_code: str = ""
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ async def search_name(name: str, state_code: str) -> NameSearchResult:
|
||||||
code = state_code.upper()
|
code = state_code.upper()
|
||||||
if code not in STATES:
|
if code not in STATES:
|
||||||
return NameSearchResult(
|
return NameSearchResult(
|
||||||
available=False,
|
available=None,
|
||||||
searched_name=name,
|
searched_name=name,
|
||||||
state_code=code,
|
state_code=code,
|
||||||
raw_response=f"Unknown 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
|
return result
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOG.error("Name search failed in %s: %s", code, exc, exc_info=True)
|
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(
|
return NameSearchResult(
|
||||||
available=False,
|
available=None,
|
||||||
searched_name=name,
|
searched_name=name,
|
||||||
state_code=code,
|
state_code=code,
|
||||||
raw_response=f"Error: {exc}",
|
raw_response=f"Error: {exc}",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue