import { format } from 'date-fns'; import type { Filing, RecentDevelopmentItem, RecentDevelopments } from '@/lib/types'; type RecentDevelopmentSourceContext = { filings: Filing[]; now?: Date; }; type RecentDevelopmentSource = { name: string; fetch: (ticker: string, context: RecentDevelopmentSourceContext) => Promise; }; type CacheEntry = { expiresAt: number; value: T; }; const RECENT_DEVELOPMENTS_CACHE_TTL_MS = 1000 * 60 * 10; const recentDevelopmentsCache = new Map>(); function filingPriority(filing: Filing) { switch (filing.filing_type) { case '8-K': return 0; case '10-Q': return 1; case '10-K': return 2; default: return 3; } } function sortFilings(filings: Filing[]) { return [...filings].sort((left, right) => { const dateDelta = Date.parse(right.filing_date) - Date.parse(left.filing_date); if (dateDelta !== 0) { return dateDelta; } return filingPriority(left) - filingPriority(right); }); } function buildTitle(filing: Filing) { switch (filing.filing_type) { case '8-K': return `${filing.company_name} filed an 8-K`; case '10-K': return `${filing.company_name} annual filing`; case '10-Q': return `${filing.company_name} quarterly filing`; default: return `${filing.company_name} filing update`; } } function buildSummary(filing: Filing) { const analysisSummary = filing.analysis?.text ?? filing.analysis?.legacyInsights ?? null; if (analysisSummary) { return analysisSummary; } const formattedDate = format(new Date(filing.filing_date), 'MMM dd, yyyy'); if (filing.filing_type === '8-K') { return `The company disclosed a current report on ${formattedDate}. Review the filing for event-specific detail and attached exhibits.`; } return `The company published a ${filing.filing_type} on ${formattedDate}. Review the filing for the latest reported business and financial changes.`; } export const secFilingsDevelopmentSource: RecentDevelopmentSource = { name: 'SEC filings', async fetch(_ticker, context) { const now = context.now ?? new Date(); const nowEpoch = now.getTime(); const recentFilings = sortFilings(context.filings) .filter((filing) => { const filedAt = Date.parse(filing.filing_date); if (!Number.isFinite(filedAt)) { return false; } const ageInDays = (nowEpoch - filedAt) / (1000 * 60 * 60 * 24); if (ageInDays > 14) { return false; } return filing.filing_type === '8-K' || filing.filing_type === '10-K' || filing.filing_type === '10-Q'; }) .slice(0, 8); return recentFilings.map((filing) => ({ id: `${filing.ticker}-${filing.accession_number}`, kind: filing.filing_type, title: buildTitle(filing), url: filing.filing_url, source: 'SEC filings', publishedAt: filing.filing_date, summary: buildSummary(filing), accessionNumber: filing.accession_number })); } }; const yahooDevelopmentSource: RecentDevelopmentSource | null = null; const investorRelationsRssSource: RecentDevelopmentSource | null = null; export async function getRecentDevelopments( ticker: string, context: RecentDevelopmentSourceContext, options?: { sources?: RecentDevelopmentSource[]; limit?: number; } ): Promise { const normalizedTicker = ticker.trim().toUpperCase(); const limit = options?.limit ?? 6; const cacheKey = `${normalizedTicker}:${context.filings.map((filing) => filing.accession_number).join(',')}`; const cached = recentDevelopmentsCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { return cached.value; } const sources = options?.sources ?? [secFilingsDevelopmentSource]; const itemCollections = await Promise.all( sources.map(async (source) => { try { return await source.fetch(normalizedTicker, context); } catch { return [] satisfies RecentDevelopmentItem[]; } }) ); const items = itemCollections .flat() .sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt)) .slice(0, limit); const result: RecentDevelopments = { status: items.length > 0 ? 'ready' : 'unavailable', items, weeklySnapshot: null }; recentDevelopmentsCache.set(cacheKey, { value: result, expiresAt: Date.now() + RECENT_DEVELOPMENTS_CACHE_TTL_MS }); return result; } export const __recentDevelopmentsInternals = { buildSummary, buildTitle, sortFilings };