diff --git a/docs/dexit-readiness-assessment.md b/docs/dexit-readiness-assessment.md index caae501..5596a0c 100644 --- a/docs/dexit-readiness-assessment.md +++ b/docs/dexit-readiness-assessment.md @@ -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"`, diff --git a/scripts/formation/base.py b/scripts/formation/base.py index 80024dd..ec37b64 100644 --- a/scripts/formation/base.py +++ b/scripts/formation/base.py @@ -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 = "" diff --git a/scripts/formation/name_search.py b/scripts/formation/name_search.py index 0ee3031..bc4937b 100644 --- a/scripts/formation/name_search.py +++ b/scripts/formation/name_search.py @@ -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}",