new-site/api/src/routes/corp-status.ts
justin f8cd37ac8c 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>
2026-04-27 06:54:22 -05:00

481 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Corporation Status Check API
*
* GET /api/v1/corp/status?name=Acme+LLC&state=WY
* Search entity_cache for corporation status (ACTIVE, DELINQUENT, etc.)
* Returns status + years behind + cost to remediate.
*
* GET /api/v1/corp/search?q=Acme&state=WY
* Fuzzy search entity_cache by name. Returns up to 10 matches.
*
* Used by:
* - Standalone Corporation Status Check tool (/tools/corporation-check)
* - FCC Compliance Check (adds corporation_status check)
*/
import { Router, type Request, type Response } from "express";
import { pool } from "../db.js";
const router = Router();
// ── RA pricing by state ──────────────────────────────────────────────────────
const RA_PRICE_CENTS: Record<string, number> = {
WY: 5000, // $50/yr Wyoming
};
const RA_DEFAULT_CENTS = 9900; // $99/yr all other states
// Our flat markup per annual report or reinstatement filing
const FILING_MARKUP_CENTS = 2500; // $25
// ── Helpers ──────────────────────────────────────────────────────────────────
interface CorpStatusResult {
found: boolean;
entity_name: string | null;
entity_number: string | null;
entity_type: string | null;
status: string | null;
formation_date: string | null;
dissolution_date: string | null;
registered_agent: string | null;
principal_address: string | null;
state: string | null;
// Computed fields
years_behind: number;
annual_report_fee_cents: number;
annual_report_frequency: string | null;
cost_per_year_cents: number;
total_catchup_cents: number;
ra_price_cents: number;
total_with_ra_cents: number;
breakdown: string;
}
/**
* Look up corporation status from entity_cache and compute remediation cost.
* Exported so fcc-lookup.ts can call it directly.
*/
export async function lookupCorpStatus(
entityName: string,
stateCode: string,
): Promise<CorpStatusResult | null> {
if (!entityName || !stateCode) return null;
const state = stateCode.toUpperCase().trim();
const name = entityName.trim();
// Try exact match first, then fuzzy (trigram)
let row: Record<string, unknown> | null = null;
try {
const exact = await pool.query(
`SELECT entity_name, entity_number, entity_type, status,
formation_date, dissolution_date, registered_agent, state
FROM entity_cache
WHERE state = $1 AND LOWER(entity_name) = LOWER($2)
LIMIT 1`,
[state, name],
);
if (exact.rows.length > 0) {
row = exact.rows[0] as Record<string, unknown>;
} else {
// Fuzzy match — require very high similarity (0.8+) to avoid false positives
// like "GTDIAL DATA SOLUTIONS" matching "SPATIAL DATA SOLUTIONS" (0.68)
const fuzzy = await pool.query(
`SELECT entity_name, entity_number, entity_type, status,
formation_date, dissolution_date, registered_agent, state,
similarity(entity_name, $2) AS sim
FROM entity_cache
WHERE state = $1 AND similarity(entity_name, $2) > 0.8
ORDER BY sim DESC
LIMIT 1`,
[state, name],
);
if (fuzzy.rows.length > 0) {
row = fuzzy.rows[0] as Record<string, unknown>;
}
}
} catch (err: any) {
// pg_trgm extension may not be available, or entity_cache empty
if (err?.code !== "42P01") {
console.warn("[corp-status] entity_cache query failed:", err?.message);
}
return null;
}
if (!row) {
// Live Playwright fallback for states without bulk data (WY, DE, etc.)
// Calls the workers /entity-status endpoint which uses the state adapter
const WORKER_URL = process.env.WORKER_URL || "http://workers:8090";
console.log(`[corp-status] Cache miss for "${name}" in ${state} — trying live search via workers`);
try {
const liveRes = await fetch(`${WORKER_URL}/entity-status`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entity_name: name, state_code: state }),
signal: AbortSignal.timeout(45000),
});
const liveData = await liveRes.json() as Record<string, unknown>;
console.log(`[corp-status] Live search result:`, JSON.stringify(liveData).slice(0, 200));
if (liveRes.ok && liveData.found && liveData.entity_name) {
row = {
entity_name: liveData.entity_name,
entity_number: liveData.entity_number || null,
entity_type: liveData.entity_type || null,
status: liveData.status || "UNKNOWN",
formation_date: liveData.formation_date || null,
dissolution_date: null,
registered_agent: liveData.registered_agent || null,
state,
};
}
} catch {
// Worker unavailable or timeout — skip this state
}
}
if (!row) return null;
const status = (row.status as string) || "UNKNOWN";
const formationDate = row.formation_date
? new Date(row.formation_date as string).toISOString().slice(0, 10)
: null;
// Look up annual report obligation for this state
let annualFee = 0;
let frequency = "annual";
let dueMonth: number | null = null;
let isAnniversary = false;
try {
const obl = await pool.query(
`SELECT fee_cents, frequency, due_month, is_anniversary
FROM state_compliance_obligations
WHERE state_code = $1 AND obligation_type = 'annual_report'
LIMIT 1`,
[state],
);
if (obl.rows.length > 0) {
const o = obl.rows[0] as Record<string, unknown>;
annualFee = (o.fee_cents as number) || 0;
frequency = (o.frequency as string) || "annual";
dueMonth = (o.due_month as number) || null;
isAnniversary = (o.is_anniversary as boolean) || false;
}
} catch {
// state_compliance_obligations table may not exist
}
// Calculate years behind
let yearsBehind = 0;
if (status !== "ACTIVE" && formationDate) {
const formed = new Date(formationDate);
const now = new Date();
const yearsExisted = now.getFullYear() - formed.getFullYear();
if (frequency === "biennial") {
yearsBehind = Math.max(1, Math.floor(yearsExisted / 2));
} else if (frequency === "annual") {
// Conservative estimate: if delinquent, assume at least 1 year behind
// Most states revoke after 2-3 years of non-filing
if (status === "DELINQUENT" || status === "SUSPENDED") {
yearsBehind = Math.max(1, Math.min(3, yearsExisted - 1));
} else if (status === "DISSOLVED" || status === "INACTIVE") {
yearsBehind = Math.max(1, Math.min(5, yearsExisted - 1));
}
}
}
// Cost calculations
const costPerYear = annualFee + FILING_MARKUP_CENTS;
const totalCatchup = yearsBehind * costPerYear;
const raPrice = RA_PRICE_CENTS[state] || RA_DEFAULT_CENTS;
const totalWithRA = totalCatchup + raPrice;
// Build human-readable breakdown
let breakdown = "";
if (yearsBehind > 0) {
const stateFeeStr = `$${(annualFee / 100).toFixed(0)}`;
const markupStr = `$${(FILING_MARKUP_CENTS / 100).toFixed(0)}`;
const perYearStr = `$${(costPerYear / 100).toFixed(0)}`;
const totalStr = `$${(totalCatchup / 100).toFixed(0)}`;
const raStr = `$${(raPrice / 100).toFixed(0)}`;
const grandStr = `$${(totalWithRA / 100).toFixed(0)}`;
if (yearsBehind === 1) {
breakdown = `Annual report: ${stateFeeStr} state fee + ${markupStr} filing = ${perYearStr}. `;
} else {
breakdown = `Annual reports (${yearsBehind} years): ${yearsBehind} × (${stateFeeStr} + ${markupStr}) = ${totalStr}. `;
}
breakdown += `Registered agent: ${raStr}/yr. Total: ${grandStr}.`;
}
return {
found: true,
entity_name: (row.entity_name as string) || null,
entity_number: (row.entity_number as string) || null,
entity_type: (row.entity_type as string) || null,
status,
formation_date: formationDate,
dissolution_date: row.dissolution_date
? new Date(row.dissolution_date as string).toISOString().slice(0, 10)
: null,
registered_agent: (row.registered_agent as string) || null,
principal_address: (row.principal_address as string) || null,
state,
years_behind: yearsBehind,
annual_report_fee_cents: annualFee,
annual_report_frequency: frequency,
cost_per_year_cents: costPerYear,
total_catchup_cents: totalCatchup,
ra_price_cents: raPrice,
total_with_ra_cents: totalWithRA,
breakdown,
};
}
// ── GET /api/v1/corp/search ──────────────────────────────────────────────────
router.get("/api/v1/corp/search", async (req: Request, res: Response) => {
const q = (req.query.q as string || "").trim();
const state = (req.query.state as string || "").toUpperCase().trim();
if (!q || q.length < 2) {
res.status(400).json({ error: "q parameter required (min 2 chars)" });
return;
}
if (!state || state.length !== 2) {
res.status(400).json({ error: "state parameter required (2-letter code)" });
return;
}
try {
const { rows } = await pool.query(
`SELECT entity_name, entity_number, entity_type, status, formation_date,
similarity(entity_name, $2) AS sim
FROM entity_cache
WHERE state = $1 AND entity_name % $2
ORDER BY sim DESC
LIMIT 10`,
[state, q],
);
res.json({
state,
query: q,
count: rows.length,
results: 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
? new Date(r.formation_date).toISOString().slice(0, 10)
: null,
})),
});
} catch (err: any) {
if (err?.code === "42P01") {
res.json({ state, query: q, count: 0, results: [], note: "Entity database not yet populated for this state." });
} else {
console.error("[corp/search] Error:", err);
res.status(500).json({ error: "Search failed" });
}
}
});
// ── GET /api/v1/corp/status ──────────────────────────────────────────────────
router.get("/api/v1/corp/status", async (req: Request, res: Response) => {
const name = (req.query.name as string || "").trim();
const state = (req.query.state as string || "").toUpperCase().trim();
if (!name) {
res.status(400).json({ error: "name parameter required" });
return;
}
if (!state || state.length !== 2) {
res.status(400).json({ error: "state parameter required (2-letter code)" });
return;
}
const result = await lookupCorpStatus(name, state);
if (!result) {
res.json({
found: false,
state,
searched_name: name,
note: "Entity not found in our database. Try the standalone state search or check directly with the Secretary of State.",
});
return;
}
res.json(result);
});
// ── GET /api/v1/corp/states ──────────────────────────────────────────────────
// Returns list of states with annual report info for the UI dropdown
router.get("/api/v1/corp/states", async (_req: Request, res: Response) => {
try {
const { rows } = await pool.query(`
SELECT s.state_code, s.fee_cents, s.frequency, s.due_description, s.is_anniversary,
(SELECT count(*) FROM entity_cache WHERE state = s.state_code) AS entity_count
FROM state_compliance_obligations s
WHERE s.obligation_type = 'annual_report'
ORDER BY s.state_code
`);
res.json({
states: rows.map((r: any) => ({
code: r.state_code,
annual_report_fee_cents: r.fee_cents,
frequency: r.frequency,
due: r.due_description,
is_anniversary: r.is_anniversary,
entities_cached: parseInt(r.entity_count, 10),
ra_price_cents: RA_PRICE_CENTS[r.state_code] || RA_DEFAULT_CENTS,
})),
});
} catch (err) {
console.error("[corp/states] Error:", err);
res.status(500).json({ error: "Failed to load state data" });
}
});
// ── POST /api/v1/corp/foreign-qual-check ────────────────────────────────────
//
// Bulk check: given an entity name, home state, and list of states served,
// check entity_cache for foreign qualification status in each state.
// Used by the 499-A intake wizard after the JurisdictionStep.
router.post("/api/v1/corp/foreign-qual-check", async (req: Request, res: Response) => {
try {
const { entity_name, home_state, states } = req.body as {
entity_name?: string;
home_state?: string;
states?: string[];
};
if (!entity_name || !states || !Array.isArray(states) || states.length === 0) {
res.status(400).json({ error: "entity_name and states[] required" });
return;
}
const homeState = (home_state || "").toUpperCase().trim();
const name = entity_name.trim();
// Check each state (skip home state — they're already formed there)
// Use entity_cache only (no live Playwright fallback — too slow for bulk)
const results: Array<{
state_code: string;
has_data: boolean;
found: boolean;
status: string | null;
entity_name_found: string | null;
needs_foreign_qual: boolean;
reason: string;
}> = [];
// Check which states have data in entity_cache so we can distinguish
// "not found" from "no data for this state"
let statesWithData: Set<string>;
try {
const dataCheck = await pool.query(
`SELECT DISTINCT state FROM entity_cache WHERE state = ANY($1)`,
[states.map(s => s.toUpperCase().trim())],
);
statesWithData = new Set(dataCheck.rows.map((r: any) => r.state));
} catch {
statesWithData = new Set();
}
for (const stateCode of states) {
const state = stateCode.toUpperCase().trim();
if (state === homeState) continue; // Skip home state
let found = false;
let status: string | null = null;
let foundName: string | null = null;
const hasData = statesWithData.has(state);
if (hasData) {
try {
// Exact match
const exact = await pool.query(
`SELECT entity_name, status FROM entity_cache
WHERE state = $1 AND LOWER(entity_name) = LOWER($2) LIMIT 1`,
[state, name],
);
if (exact.rows.length > 0) {
found = true;
status = (exact.rows[0] as any).status;
foundName = (exact.rows[0] as any).entity_name;
} else {
// Fuzzy match
const fuzzy = await pool.query(
`SELECT entity_name, status, similarity(entity_name, $2) AS sim
FROM entity_cache
WHERE state = $1 AND similarity(entity_name, $2) > 0.8
ORDER BY sim DESC LIMIT 1`,
[state, name],
);
if (fuzzy.rows.length > 0) {
found = true;
status = (fuzzy.rows[0] as any).status;
foundName = (fuzzy.rows[0] as any).entity_name;
}
}
} catch {
// entity_cache or pg_trgm not available — skip
}
}
let needsFQ = false;
let reason = "";
if (!hasData) {
// No entity_cache data for this state — can't determine, skip
reason = "No data available for this state yet";
} else if (!found) {
needsFQ = true;
reason = "No foreign corporation registration found in this state";
} else if (status && ["DISSOLVED", "REVOKED", "CANCELLED", "SUSPENDED", "INACTIVE"].includes(status.toUpperCase())) {
needsFQ = true;
reason = `Registration found but status is ${status} — reinstatement or new filing needed`;
} else if (status && status.toUpperCase() === "DELINQUENT") {
needsFQ = true;
reason = `Registration found but delinquent — annual report or reinstatement needed`;
}
results.push({
state_code: state,
has_data: hasData,
found,
status,
entity_name_found: foundName,
needs_foreign_qual: needsFQ,
reason,
});
}
const missing = results.filter((r) => r.needs_foreign_qual);
res.json({
entity_name: name,
home_state: homeState,
total_states_checked: results.length,
states_missing: missing.length,
results,
foreign_qual_service_fee_cents: 9900, // $99/state for multi-state
});
} catch (err) {
console.error("[corp/foreign-qual-check] Error:", err);
res.status(500).json({ error: "Foreign qualification check failed" });
}
});
export default router;