Rebuild company overview analysis page
This commit is contained in:
@@ -70,6 +70,10 @@ import {
|
||||
upsertWatchlistItemRecord
|
||||
} from '@/lib/server/repos/watchlist';
|
||||
import { getPriceHistory, getQuote } from '@/lib/server/prices';
|
||||
import { synthesizeCompanyOverview } from '@/lib/server/company-overview-synthesis';
|
||||
import { getRecentDevelopments } from '@/lib/server/recent-developments';
|
||||
import { deriveValuationSnapshot, getSecCompanyProfile, toCompanyProfile } from '@/lib/server/sec-company-profile';
|
||||
import { getCompanyDescription } from '@/lib/server/sec-description';
|
||||
import { answerSearchQuery, searchKnowledgeBase } from '@/lib/server/search';
|
||||
import {
|
||||
enqueueTask,
|
||||
@@ -1362,13 +1366,15 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
return jsonError('ticker is required');
|
||||
}
|
||||
|
||||
const [filings, holding, watchlistItem, liveQuote, priceHistory, journalPreview] = await Promise.all([
|
||||
const [filings, holding, watchlistItem, liveQuote, priceHistory, journalPreview, memo, secProfile] = await Promise.all([
|
||||
listFilingsRecords({ ticker, limit: 40 }),
|
||||
getHoldingByTicker(session.user.id, ticker),
|
||||
getWatchlistItemByTicker(session.user.id, ticker),
|
||||
getQuote(ticker),
|
||||
getPriceHistory(ticker),
|
||||
listResearchJournalEntries(session.user.id, ticker, 6)
|
||||
listResearchJournalEntries(session.user.id, ticker, 6),
|
||||
getResearchMemoByTicker(session.user.id, ticker),
|
||||
getSecCompanyProfile(ticker)
|
||||
]);
|
||||
const redactedFilings = filings
|
||||
.map(redactInternalFilingAnalysisFields)
|
||||
@@ -1376,6 +1382,7 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
|
||||
const latestFiling = redactedFilings[0] ?? null;
|
||||
const companyName = latestFiling?.company_name
|
||||
?? secProfile?.companyName
|
||||
?? holding?.company_name
|
||||
?? watchlistItem?.company_name
|
||||
?? ticker;
|
||||
@@ -1416,6 +1423,11 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
? (referenceMetrics.netIncome / referenceMetrics.revenue) * 100
|
||||
: null
|
||||
};
|
||||
const annualFiling = redactedFilings.find((entry) => entry.filing_type === '10-K') ?? null;
|
||||
const [description, synthesizedDevelopments] = await Promise.all([
|
||||
getCompanyDescription(annualFiling),
|
||||
getRecentDevelopments(ticker, { filings: redactedFilings })
|
||||
]);
|
||||
const latestFilingSummary = latestFiling
|
||||
? {
|
||||
accessionNumber: latestFiling.accession_number,
|
||||
@@ -1427,6 +1439,31 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
hasAnalysis: Boolean(latestFiling.analysis?.text || latestFiling.analysis?.legacyInsights)
|
||||
}
|
||||
: null;
|
||||
const companyProfile = toCompanyProfile(secProfile, description);
|
||||
const valuationSnapshot = deriveValuationSnapshot({
|
||||
quote: liveQuote,
|
||||
sharesOutstanding: secProfile?.sharesOutstanding ?? null,
|
||||
revenue: keyMetrics.revenue,
|
||||
cash: keyMetrics.cash,
|
||||
debt: keyMetrics.debt,
|
||||
netIncome: keyMetrics.netIncome
|
||||
});
|
||||
const synthesis = await synthesizeCompanyOverview({
|
||||
ticker,
|
||||
companyName,
|
||||
description,
|
||||
memo,
|
||||
latestFilingSummary,
|
||||
recentAiReports: aiReports.slice(0, 5),
|
||||
recentDevelopments: synthesizedDevelopments.items
|
||||
});
|
||||
const recentDevelopments = {
|
||||
...synthesizedDevelopments,
|
||||
weeklySnapshot: synthesis.weeklySnapshot,
|
||||
status: synthesizedDevelopments.items.length > 0
|
||||
? synthesis.weeklySnapshot ? 'ready' : 'partial'
|
||||
: synthesis.weeklySnapshot ? 'partial' : 'unavailable'
|
||||
} as const;
|
||||
|
||||
return Response.json({
|
||||
analysis: {
|
||||
@@ -1453,7 +1490,11 @@ export const app = new Elysia({ prefix: '/api' })
|
||||
journalPreview,
|
||||
recentAiReports: aiReports.slice(0, 5),
|
||||
latestFilingSummary,
|
||||
keyMetrics
|
||||
keyMetrics,
|
||||
companyProfile,
|
||||
valuationSnapshot,
|
||||
bullBear: synthesis.bullBear,
|
||||
recentDevelopments
|
||||
}
|
||||
});
|
||||
}, {
|
||||
|
||||
@@ -485,6 +485,14 @@ if (process.env.RUN_TASK_WORKFLOW_E2E === '1') {
|
||||
latestFilingSummary: { accessionNumber: string; summary: string | null } | null;
|
||||
keyMetrics: { revenue: number | null; netMargin: number | null };
|
||||
position: { company_name: string | null } | null;
|
||||
companyProfile: { source: string; description: string | null };
|
||||
valuationSnapshot: { source: string; marketCap: number | null; evToRevenue: number | null };
|
||||
bullBear: { source: string; bull: string[]; bear: string[] };
|
||||
recentDevelopments: {
|
||||
status: string;
|
||||
items: Array<{ kind: string; accessionNumber: string | null }>;
|
||||
weeklySnapshot: { source: string; itemCount: number } | null;
|
||||
};
|
||||
};
|
||||
}).analysis;
|
||||
|
||||
@@ -499,6 +507,12 @@ if (process.env.RUN_TASK_WORKFLOW_E2E === '1') {
|
||||
expect(payload.keyMetrics.revenue).toBe(41000000000);
|
||||
expect(payload.keyMetrics.netMargin).not.toBeNull();
|
||||
expect(payload.position?.company_name).toBe('Netflix, Inc.');
|
||||
expect(['sec_derived', 'unavailable']).toContain(payload.companyProfile.source);
|
||||
expect(['derived', 'partial', 'unavailable']).toContain(payload.valuationSnapshot.source);
|
||||
expect(['ai_synthesized', 'memo_fallback', 'unavailable']).toContain(payload.bullBear.source);
|
||||
expect(['ready', 'partial', 'unavailable']).toContain(payload.recentDevelopments.status);
|
||||
expect(payload.recentDevelopments.items[0]?.accessionNumber).toBe('0000000000-26-000777');
|
||||
expect(payload.recentDevelopments.weeklySnapshot?.itemCount ?? 0).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const updatedEntry = await jsonRequest('PATCH', `/api/research/journal/${entryId}`, {
|
||||
title: 'Thesis refresh v2',
|
||||
|
||||
Reference in New Issue
Block a user