Skip to content

feat: unified entity search + related-persons graph - #154

Merged
munisp merged 3 commits into
mainfrom
feat/entity-search
Sep 13, 2026
Merged

feat: unified entity search + related-persons graph#154
munisp merged 3 commits into
mainfrom
feat/entity-search

Conversation

@munisp

@munisp munisp commented Sep 13, 2026

Copy link
Copy Markdown
Owner

What

WP1 feat/entity-search — unified one-box entity search + related-persons graph, closing the Intelius one-box search + related-persons graph gap.

New tRPC router entitySearchRouter in server/entitySearch.ts:

  1. entitySearch.search (query, protectedProcedure) — single string input with automatic type detection:
    • 11 digits (not starting with 0) → dual NIN + BVN gateway lookup (/v1/nin/:id, /v1/bvn/:id)
    • RC prefix → CAC gateway lookup + tenant's corporate_screening_profiles
    • Nigerian phone patterns (0… / 234… / +234…, all variants matched) → investigations / kyc_records / candidate_profiles phone columns
    • otherwise → fuzzy ILIKE name search over the same three tables
    • All sources run in parallel with Promise.allSettled — a failing source (e.g. gateway outage) is recorded as {source, status: "error", latencyMs, error} and never sinks the other sources. Returns { queryType, identities, investigations, kyc, sources }.
    • Every search is audit-logged (audit_log, category api, HMAC integrity hash, who/what/queryType/per-source status).
  2. entitySearch.getAssociates (query) — input investigationRef and/or candidateId. Assembles a related-persons graph from: beneficial owners/directors in corporate_screening_profiles (CAC payloads), informal_references on the subject's informal verification cases, and shared phone/address matches in kyc_records/investigations. Returns { nodes, edges, sources }. Confidence rules: direct_documented (beneficial owner, signed guarantor) → high; declared (self-nominated referee, landlord, association…) → medium; shared_attribute (same phone/address) → low. Graph views are audit-logged too.
  3. entitySearch.searchHistory (query) — current tenant's audit-logged searches, keyset-paginated (limit/cursor).

Why

Intelius-parity gap: operators currently have to know which subsystem to query per identifier type. One box now detects the identifier and fans out across all relevant sources, and the associates graph surfaces relationships (documented vs declared vs circumstantial) that are otherwise invisible.

Security / tenancy

  • All rows tenant-scoped from ctx.tenantId (never client-supplied); no tenant context → FORBIDDEN; anonymous → UNAUTHORIZED.
  • Gateway call fails closed when BIS_GATEWAY_URL is unconfigured.
  • Audit-write failure degrades to a logged warning (consistent with writeAuditLog in routers.ts); the search itself is never blocked by audit.
  • ILIKE inputs escape %/_; all queries parameterized. No new tables, no secrets, no .github/workflows changes.

How tested (real output)

pnpm install (pnpm 10.27), then with the registration patch below applied locally:

$ npx tsc --noEmit            # 0 errors in entitySearch.ts / entity-search.test.ts / routers.ts
$ npx vitest run server/entity-search.test.ts

 ✓ server/entity-search.test.ts (37 tests) 37ms

 Test Files  1 passed (1)
      Tests  37 passed (37)
   Duration  662ms

Coverage: query-type detection matrix (14 cases), RC/phone normalization, confidence classification (guarantor→high, declared→medium, shared→low), beneficial-owner payload extraction, per-source failure isolation (gateway outage leaves DB sources ok), tenant isolation (every SQL carries the ctx tenant; null tenant → FORBIDDEN), NOT_FOUND for cross-tenant refs, search-history pagination.

⚠️ Integration patch required — server/routers.ts (374KB, too large for MCP push; verified locally with typecheck + tests above)

The branch intentionally does not modify server/routers.ts. Apply this exact 2-line patch on main at merge/integration time:

--- a/server/routers.ts
+++ b/server/routers.ts
@@ -142,6 +142,7 @@ import { piiKeyCustodyRouter } from "./piiKeyCustody";
+import { entitySearchRouter } from "./entitySearch";
@@ registration object, last entry @@
   kycDocumentEvidence: kycDocumentEvidenceRouter,
+  entitySearch: entitySearchRouter,
 });
  • Import anchor: insert import { entitySearchRouter } from "./entitySearch"; immediately after line 142 import { piiKeyCustodyRouter } from "./piiKeyCustody";
  • Registration anchor: insert entitySearch: entitySearchRouter, immediately after the line kycDocumentEvidence: kycDocumentEvidenceRouter, (currently the last entry of the appRouter = router({...}) object, ~line 7692)

Files changed

  • server/entitySearch.ts (new) — router + exported pure helpers (detectQueryType, normalizeRcNumber, phoneVariants, confidenceForEvidenceKind, evidenceKindForReferenceType, extractBeneficialOwners)
  • server/entity-search.test.ts (new) — 37 tests

Risks

  • Registration patch must be applied at integration (above) or the endpoints are unreachable — everything else is self-contained.
  • Gateway identity data shapes are passed through opaquely; extractBeneficialOwners handles the known CAC payload shapes and returns [] on anything malformed (fail-safe, tested).
  • Client UI is a separate WP; AppRouter type picks up entitySearch automatically once the patch lands.

@munisp
munisp merged commit 25a7981 into main Sep 13, 2026
9 of 10 checks passed
munisp added a commit that referenced this pull request Sep 14, 2026
…gration) (#160)

- WP1 (#154): entitySearchRouter import + appRouter registration
- WP2 (#158): monitoringRouter import + appRouter registration
- WP3 (#155): subjectPortalRouter + computeDataCompleteness imports; subjectPortal registration; removed routers.ts-local getFallbackSuggestion (now shared in server/dataCompleteness.ts); getDataCompleteness delegates to computeDataCompleteness; consentPurposeEnum gains consumer_self_check; subjectAccessTokens/subjectDisputes pgTable declarations (matches drizzle/0023_subject_portal.sql)
- WP4 (#157): shareableReportsRouter + selfServiceBillingRouter imports + registrations; reportShareLinks/planSignups pgTable declarations (matches drizzle/0024_share_links_and_plan_signups.sql)
- WP5 (#156): lookup.phone procedure (gatewayFetch /v1/phone/:number, validated input)

Co-authored-by: bis-integration <integration@bis.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant