Backfill sparse report sections, add enrichment section refresh, and bootstrap first-admin

Reports: the LLM reliably used company_enrichment for prose fields but
inconsistently populated the parallel Finding-list/string-list fields from
the same evidence, even with progressively more explicit prompting. Add a
code-level backfill (products, recent developments, financial signals,
strategic initiatives, regulatory signals, risks/opportunities mirrored
from SWOT, unknowns, monitoring recommendations) that only ever fills in
what the model left empty, never overwrites what it produced.

Enrichment tab: reorder sections (Products/Recent updates before
Customers/Competitors) and add a per-section "Refresh" button that
re-fetches just one of NinjaPear's six independent per-company endpoints
when it came back empty - confirmed live that a data-coverage gap (e.g.
Amazon returning no products) is real provider behavior, not a bug.

Auth: the first account registered on a deployment with zero existing
admins is now auto-promoted to admin, closing the chicken-and-egg gap
where the only path to admin access was direct DB access. Self-heals if
the last admin ever deletes their account.

Also bumps nginx's proxy_read_timeout for api.ciagent.org to cover the
enrichment refresh's synchronous funding-endpoint call (up to 5 minutes
per NinjaPear's docs).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-06 17:41:02 -04:00
co-authored by Claude Sonnet 5
parent 1be3e53584
commit 18305b545c
18 changed files with 1087 additions and 53 deletions
+73
View File
@@ -3,6 +3,9 @@
from __future__ import annotations
import uuid
from unittest.mock import patch
import pytest
def _register_and_login(client) -> dict[str, str]:
@@ -252,3 +255,73 @@ def test_create_company_does_not_enqueue_enrichment_without_a_key(client):
assert resp.status_code == 201
mock_delay.assert_not_called()
assert resp.json()["enrichment"] is None
def test_refresh_enrichment_section_requires_authentication(client):
resp = client.post(f"/api/v1/companies/{uuid.uuid4()}/enrichment/sections/products/refresh")
assert resp.status_code == 401
def test_refresh_enrichment_section_rejects_an_unknown_section(client):
headers = _register_and_login(client)
company = _create_company(client, headers).json()
resp = client.post(
f"/api/v1/companies/{company['id']}/enrichment/sections/not_a_section/refresh",
headers=headers,
)
assert resp.status_code == 422
def test_refresh_enrichment_section_404s_without_an_enrichment_record(client):
headers = _register_and_login(client)
company = _create_company(client, headers).json() # no NINJAPEAR_API_KEY -> no record
resp = client.post(
f"/api/v1/companies/{company['id']}/enrichment/sections/products/refresh",
headers=headers,
)
assert resp.status_code == 404
def test_refresh_enrichment_section_is_scoped_to_owner(client):
owner_headers = _register_and_login(client)
other_headers = _register_and_login(client)
company = _create_company(client, owner_headers).json()
resp = client.post(
f"/api/v1/companies/{company['id']}/enrichment/sections/products/refresh",
headers=other_headers,
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_refresh_enrichment_section_updates_just_that_section(client, db_session, settings):
from app.enrichment.base import Product
from app.enrichment.mock import MockEnrichmentProvider
from app.repositories.company_repository import CompanyRepository
from app.services.enrichment_service import enrich_company
headers = _register_and_login(client)
company_id = _create_company(client, headers).json()["id"]
company = await CompanyRepository(db_session).get_by_id(uuid.UUID(company_id))
await enrich_company(db_session, settings, MockEnrichmentProvider(), company)
class _ProductsProvider(MockEnrichmentProvider):
async def get_products(self, name, website):
return [Product(name="Acme Pay", description="Payments product")]
with patch("app.api.v1.companies.get_enrichment_provider", return_value=_ProductsProvider()):
resp = client.post(
f"/api/v1/companies/{company_id}/enrichment/sections/products/refresh",
headers=headers,
)
assert resp.status_code == 200
products = resp.json()["enrichment"]["data"]["products"]
assert products == [{"name": "Acme Pay", "description": "Payments product", "category": None}]