Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
191
api/src/routes/entities.ts
Normal file
191
api/src/routes/entities.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
// entities.ts — Internal API for Verilex Data entity sync
|
||||
//
|
||||
// GET /api/v1/entities/bulk?state=CO&limit=10000&cursor=123 — paginated bulk export
|
||||
// GET /api/v1/entities/states — list states with entity counts
|
||||
// GET /api/v1/states/:code/name-search?name=Acme — name availability (cached 24h)
|
||||
//
|
||||
// All endpoints require internal auth (PW_INTERNAL_API_KEY).
|
||||
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { pool } from "../db.js";
|
||||
import { internalAuth } from "../middleware/internal-auth.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
|
||||
|
||||
// ── Bulk entity export ───────────────────────────────────────────────────────
|
||||
|
||||
router.get("/api/v1/entities/bulk", internalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const state = (req.query.state as string || "").toUpperCase();
|
||||
const limit = Math.min(parseInt(req.query.limit as string || "10000", 10), 50000);
|
||||
const cursor = parseInt(req.query.cursor as string || "0", 10);
|
||||
|
||||
if (!state || state.length !== 2) {
|
||||
res.status(400).json({ error: "state parameter required (2-letter code)" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, jurisdiction, entity_name, entity_number, entity_type, status,
|
||||
formation_date, dissolution_date, registered_agent, principal_address, state
|
||||
FROM entity_cache
|
||||
WHERE state = $1 AND id > $2
|
||||
ORDER BY id
|
||||
LIMIT $3`,
|
||||
[state, cursor, limit],
|
||||
);
|
||||
|
||||
const nextCursor = rows.length > 0 ? rows[rows.length - 1].id : cursor;
|
||||
const hasMore = rows.length === limit;
|
||||
|
||||
res.json({
|
||||
state,
|
||||
count: rows.length,
|
||||
has_more: hasMore,
|
||||
next_cursor: nextCursor,
|
||||
entities: rows.map((r: any) => ({
|
||||
entity_name: r.entity_name,
|
||||
entity_number: r.entity_number,
|
||||
entity_type: r.entity_type,
|
||||
status: r.status,
|
||||
formation_date: r.formation_date,
|
||||
dissolution_date: r.dissolution_date,
|
||||
registered_agent: r.registered_agent,
|
||||
principal_address: r.principal_address,
|
||||
jurisdiction: r.jurisdiction,
|
||||
state: r.state,
|
||||
})),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Bulk export error:", err.message);
|
||||
res.status(500).json({ error: "Bulk export failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// ── List states with entity counts ───────────────────────────────────────────
|
||||
|
||||
router.get("/api/v1/entities/states", internalAuth, async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT state, COUNT(*) AS count, MAX(last_synced) AS last_synced
|
||||
FROM entity_cache
|
||||
GROUP BY state
|
||||
ORDER BY count DESC`,
|
||||
);
|
||||
|
||||
res.json({
|
||||
total_states: rows.length,
|
||||
total_entities: rows.reduce((sum: number, r: any) => sum + parseInt(r.count, 10), 0),
|
||||
states: rows.map((r: any) => ({
|
||||
state: r.state,
|
||||
count: parseInt(r.count, 10),
|
||||
last_synced: r.last_synced,
|
||||
})),
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: "Failed to list states" });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Name availability search (cached 24h) ────────────────────────────────────
|
||||
|
||||
router.get("/api/v1/states/:code/name-search", internalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const stateCode = req.params.code.toUpperCase();
|
||||
const name = (req.query.name as string || "").trim();
|
||||
|
||||
if (!name || name.length < 2) {
|
||||
res.status(400).json({ error: "name parameter required (min 2 chars)" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const { rows: cached } = await pool.query(
|
||||
`SELECT available, exact_match, similar_names, searched_at
|
||||
FROM name_search_cache
|
||||
WHERE state_code = $1 AND searched_name = $2 AND expires_at > NOW()`,
|
||||
[stateCode, name.toUpperCase()],
|
||||
);
|
||||
|
||||
if (cached.length > 0) {
|
||||
res.json({
|
||||
state_code: stateCode,
|
||||
name,
|
||||
available: cached[0].available,
|
||||
exact_match: cached[0].exact_match,
|
||||
similar_names: cached[0].similar_names || [],
|
||||
cached: true,
|
||||
searched_at: cached[0].searched_at,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Call Python worker for live search
|
||||
let searchResult: any;
|
||||
try {
|
||||
const workerResp = await fetch(`${WORKER_URL}/name-search`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ state_code: stateCode, name }),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
searchResult = await workerResp.json();
|
||||
} catch (workerErr: any) {
|
||||
// Worker unavailable — fall back to entity_cache search
|
||||
const { rows: fallback } = await pool.query(
|
||||
`SELECT entity_name FROM entity_cache
|
||||
WHERE state = $1 AND upper(entity_name) = $2
|
||||
LIMIT 1`,
|
||||
[stateCode, name.toUpperCase()],
|
||||
);
|
||||
|
||||
const { rows: similar } = await pool.query(
|
||||
`SELECT entity_name FROM entity_cache
|
||||
WHERE state = $1 AND entity_name ILIKE $2
|
||||
ORDER BY entity_name LIMIT 10`,
|
||||
[stateCode, `%${name}%`],
|
||||
);
|
||||
|
||||
searchResult = {
|
||||
available: fallback.length === 0,
|
||||
exact_match: fallback.length > 0,
|
||||
similar_names: similar.map((r: any) => r.entity_name),
|
||||
source: "cache_fallback",
|
||||
};
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
const available = searchResult.available ?? null;
|
||||
const exactMatch = searchResult.exact_match ?? false;
|
||||
const similarNames = searchResult.similar_names || [];
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO name_search_cache (state_code, searched_name, available, exact_match, similar_names)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (state_code, searched_name) DO UPDATE SET
|
||||
available = EXCLUDED.available,
|
||||
exact_match = EXCLUDED.exact_match,
|
||||
similar_names = EXCLUDED.similar_names,
|
||||
searched_at = NOW(),
|
||||
expires_at = NOW() + INTERVAL '24 hours'`,
|
||||
[stateCode, name.toUpperCase(), available, exactMatch, similarNames],
|
||||
).catch(() => {}); // non-critical if cache write fails
|
||||
|
||||
res.json({
|
||||
state_code: stateCode,
|
||||
name,
|
||||
available,
|
||||
exact_match: exactMatch,
|
||||
similar_names: similarNames,
|
||||
cached: false,
|
||||
searched_at: new Date().toISOString(),
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Name search error:", err.message);
|
||||
res.status(500).json({ error: "Name search failed" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Add table
Add a link
Reference in a new issue