fix(formation): add working /name-search worker route + e2e harness
Two latent bugs the e2e harness caught:
1. api entities.ts GET /states/:code/name-search calls WORKER_URL/name-search,
but job_server had NO such route -> 404 -> silently fell back to stale
entity_cache on every live name check. Added a synchronous /name-search route
returning {available,exact_match,similar_names,state}.
2. both the new route AND the existing async handle_name_search imported a
nonexistent search_name_sync(); fixed to drive the real async search_name()
via an event loop (same pattern as /entity-status).
scripts/e2e-formation-order.mjs: replays the real formation order flow (live
name search -> formation_orders insert -> ERPNext customer + Sales Order with
BUSINESS-FORMATION + STATE-FILING-FEE line items -> verify SO total + DB linkage
-> cleanup) without a real Stripe charge or state filing. Run in the api container.
Also created the missing ERPNext Items (BUSINESS-FORMATION, STATE-FILING-FEE,
FOREIGN-QUAL-SINGLE/MULTI) that the formation SO references.
This commit is contained in:
parent
c0344769a0
commit
4c0decd175
2 changed files with 244 additions and 2 deletions
|
|
@ -224,7 +224,8 @@ def _check_standard_delay(order_name: str, checkpoint: str) -> dict | None:
|
|||
|
||||
def handle_name_search(payload: dict) -> dict:
|
||||
"""Search for business name availability in a state portal."""
|
||||
from scripts.formation.name_search import search_name_sync
|
||||
import asyncio
|
||||
from scripts.formation.name_search import search_name
|
||||
order_name = payload["order_name"]
|
||||
state_code = payload["state_code"]
|
||||
entity_name = payload["entity_name"]
|
||||
|
|
@ -235,7 +236,12 @@ def handle_name_search(payload: dict) -> dict:
|
|||
return {"action": "name_search", **defer}
|
||||
|
||||
LOG.info("Name search: %s in %s", entity_name, state_code)
|
||||
result = search_name_sync(entity_name, state_code)
|
||||
_loop = asyncio.new_event_loop()
|
||||
try:
|
||||
_nsr = _loop.run_until_complete(search_name(entity_name, state_code))
|
||||
finally:
|
||||
_loop.close()
|
||||
result = vars(_nsr) if hasattr(_nsr, "__dict__") else dict(_nsr)
|
||||
|
||||
# Report back to ERPNext
|
||||
from scripts.workers.erpnext_client import ERPNextClient
|
||||
|
|
@ -2014,6 +2020,16 @@ class JobHandler(BaseHTTPRequestHandler):
|
|||
self._handle_rmd_preview()
|
||||
return
|
||||
|
||||
if self.path == "/name-search":
|
||||
# Synchronous name-availability check used by the API
|
||||
# (GET /api/v1/states/:code/name-search). Returns the shape
|
||||
# entities.ts expects: {available, state, exact_match, similar_names}.
|
||||
# This is distinct from /entity-status (which frames the same
|
||||
# search as an entity-status lookup) and from the order-gated
|
||||
# async name_search job (which writes back to ERPNext).
|
||||
self._handle_name_search_sync()
|
||||
return
|
||||
|
||||
if self.path == "/jobs/presign":
|
||||
# Synchronous presign — used by API routes
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
|
|
@ -2091,6 +2107,48 @@ class JobHandler(BaseHTTPRequestHandler):
|
|||
|
||||
self._respond(202, {"job_id": job_id, "action": action, "status": "queued"})
|
||||
|
||||
def _handle_name_search_sync(self):
|
||||
"""Synchronous name-availability check used by the API.
|
||||
POST /name-search {state_code, name}
|
||||
Returns {available, state, exact_match, similar_names} (the shape
|
||||
api/src/routes/entities.ts expects). Calls the live state portal
|
||||
adapter via search_name_sync; the API caches the result for 24h.
|
||||
"""
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length)
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self._respond(400, {"error": "Invalid JSON"})
|
||||
return
|
||||
|
||||
state_code = payload.get("state_code", "").strip().upper()
|
||||
name = (payload.get("name") or payload.get("entity_name") or "").strip()
|
||||
if not state_code or not name:
|
||||
self._respond(400, {"error": "state_code and name required"})
|
||||
return
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
from scripts.formation.name_search import search_name
|
||||
loop = asyncio.new_event_loop()
|
||||
result = loop.run_until_complete(search_name(name, state_code))
|
||||
loop.close()
|
||||
if hasattr(result, "__dict__"):
|
||||
result = vars(result)
|
||||
elif hasattr(result, "_asdict"):
|
||||
result = result._asdict()
|
||||
self._respond(200, {
|
||||
"available": result.get("available"),
|
||||
"exact_match": result.get("exact_match"),
|
||||
"similar_names": result.get("similar_names", []),
|
||||
"state": state_code,
|
||||
"source": "live_search",
|
||||
})
|
||||
except Exception as exc:
|
||||
LOG.exception("name-search sync failed for %s/%s", state_code, name)
|
||||
self._respond(500, {"error": str(exc)})
|
||||
|
||||
def _handle_entity_status(self):
|
||||
"""Synchronous entity status lookup via Playwright state adapter.
|
||||
POST /entity-status {entity_name, state_code}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue