diff --git a/lib/server/prices.ts b/lib/server/prices.ts index 143fa99..5ad3c79 100644 --- a/lib/server/prices.ts +++ b/lib/server/prices.ts @@ -1,3 +1,5 @@ +import { normalizeTicker } from '@/lib/server/utils'; + const YAHOO_BASE = 'https://query1.finance.yahoo.com/v8/finance/chart'; const QUOTE_CACHE_TTL_MS = 1000 * 60; const PRICE_HISTORY_CACHE_TTL_MS = 1000 * 60 * 15; @@ -27,11 +29,11 @@ const quoteCache = new Map(); const priceHistoryCache = new Map(); function buildYahooChartUrl(ticker: string, params: string) { - return `${YAHOO_BASE}/${encodeURIComponent(ticker.trim().toUpperCase())}?${params}`; + return `${YAHOO_BASE}/${encodeURIComponent(normalizeTicker(ticker))}?${params}`; } export async function getQuote(ticker: string): Promise { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); const cached = quoteCache.get(normalizedTicker); if (cached && cached.expiresAt > Date.now()) { @@ -101,7 +103,7 @@ export async function getQuoteOrNull(ticker: string): Promise { } export async function getHistoricalClosingPrices(ticker: string, dates: string[]) { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); const normalizedDates = dates .map((value) => { const parsed = Date.parse(value); @@ -169,7 +171,7 @@ export async function getHistoricalClosingPrices(ticker: string, dates: string[] } export async function getPriceHistory(ticker: string): Promise { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); const cached = priceHistoryCache.get(normalizedTicker); if (cached && cached.expiresAt > Date.now()) { diff --git a/lib/server/recent-developments.ts b/lib/server/recent-developments.ts index a72cc60..7ec3ce9 100644 --- a/lib/server/recent-developments.ts +++ b/lib/server/recent-developments.ts @@ -1,5 +1,6 @@ import { format } from 'date-fns'; import type { Filing, RecentDevelopmentItem, RecentDevelopments } from '@/lib/types'; +import { normalizeTicker } from '@/lib/server/utils'; export type RecentDevelopmentSourceContext = { filings: Filing[]; @@ -115,9 +116,9 @@ export async function getRecentDevelopments( limit?: number; } ): Promise { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); const limit = options?.limit ?? 6; - const cacheKey = `${normalizedTicker}:${context.filings.map((filing) => filing.accession_number).join(',')}`; + const cacheKey = `${normalizedTicker ?? ''}:${context.filings.map((filing) => filing.accession_number).join(',')}`; const cached = recentDevelopmentsCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { @@ -128,7 +129,7 @@ export async function getRecentDevelopments( const itemCollections = await Promise.all( sources.map(async (source) => { try { - return await source.fetch(normalizedTicker, context); + return await source.fetch(normalizedTicker ?? '', context); } catch { return [] satisfies RecentDevelopmentItem[]; } diff --git a/lib/server/repos/filings.ts b/lib/server/repos/filings.ts index 9870172..4f64fc4 100644 --- a/lib/server/repos/filings.ts +++ b/lib/server/repos/filings.ts @@ -2,6 +2,7 @@ import { desc, eq, inArray, max } from 'drizzle-orm'; import type { Filing } from '@/lib/types'; import { db } from '@/lib/server/db'; import { filing, filingLink } from '@/lib/server/db/schema'; +import { normalizeTicker, nowIso } from '@/lib/server/utils'; type FilingRow = typeof filing.$inferSelect; @@ -90,7 +91,7 @@ export async function getFilingByAccession(accessionNumber: string) { export async function listLatestFilingDatesByTickers(tickers: string[]) { const normalizedTickers = [...new Set( tickers - .map((ticker) => ticker.trim().toUpperCase()) + .map((ticker) => normalizeTicker(ticker)) .filter((ticker) => ticker.length > 0) )]; @@ -121,7 +122,7 @@ export async function upsertFilingsRecords(items: UpsertFilingInput[]) { let updated = 0; for (const item of items) { - const now = new Date().toISOString(); + const now = nowIso(); const existing = await getFilingByAccession(item.accession_number); @@ -192,7 +193,7 @@ export async function saveFilingAnalysis( .update(filing) .set({ analysis, - updated_at: new Date().toISOString() + updated_at: nowIso() }) .where(eq(filing.accession_number, accessionNumber)) .returning(); @@ -208,7 +209,7 @@ export async function updateFilingMetricsById( .update(filing) .set({ metrics, - updated_at: new Date().toISOString() + updated_at: nowIso() }) .where(eq(filing.id, filingId)) .returning(); diff --git a/lib/server/repos/holdings.ts b/lib/server/repos/holdings.ts index 64e5f36..a5a6fa3 100644 --- a/lib/server/repos/holdings.ts +++ b/lib/server/repos/holdings.ts @@ -3,6 +3,7 @@ import type { Holding } from '@/lib/types'; import { recalculateHolding } from '@/lib/server/portfolio'; import { db } from '@/lib/server/db'; import { filing, holding, watchlistItem } from '@/lib/server/db/schema'; +import { normalizeTicker, nowIso } from '@/lib/server/utils'; type HoldingRow = typeof holding.$inferSelect; @@ -30,7 +31,7 @@ function sortByMarketValueDesc(rows: Holding[]) { function normalizeHoldingInput(input: { ticker: string; shares: number; avgCost: number; currentPrice: number }) { return { - ticker: input.ticker.trim().toUpperCase(), + ticker: normalizeTicker(input.ticker), shares: input.shares.toFixed(6), avg_cost: input.avgCost.toFixed(6), current_price: input.currentPrice.toFixed(6) @@ -82,7 +83,7 @@ export async function listUserHoldings(userId: string) { } export async function getHoldingByTicker(userId: string, ticker: string) { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } @@ -104,8 +105,8 @@ export async function upsertHoldingRecord(input: { currentPrice?: number; companyName?: string; }) { - const ticker = input.ticker.trim().toUpperCase(); - const now = new Date().toISOString(); + const ticker = normalizeTicker(input.ticker); + const now = nowIso(); const [existing] = await db .select() @@ -251,8 +252,8 @@ export async function updateHoldingByIdRecord(input: { shares: shares.toFixed(6), avg_cost: avgCost.toFixed(6), current_price: currentPrice.toFixed(6), - updated_at: new Date().toISOString(), - last_price_at: new Date().toISOString() + updated_at: nowIso(), + last_price_at: nowIso() }); const [updated] = await db diff --git a/lib/server/repos/research-library.ts b/lib/server/repos/research-library.ts index a6f8ea0..05862a2 100644 --- a/lib/server/repos/research-library.ts +++ b/lib/server/repos/research-library.ts @@ -27,6 +27,14 @@ import { } from '@/lib/server/db/schema'; import { getFilingByAccession, listFilingsRecords } from '@/lib/server/repos/filings'; import { getWatchlistItemByTicker } from '@/lib/server/repos/watchlist'; +import { + normalizeTicker, + normalizeTags, + normalizeOptionalString, + normalizeRecord, + normalizePositiveInteger, + nowIso +} from '@/lib/server/utils'; type ResearchArtifactRow = typeof researchArtifact.$inferSelect; type ResearchMemoRow = typeof researchMemo.$inferSelect; @@ -51,55 +59,6 @@ const RESEARCH_PACKET_SECTION_TITLES: Record = { next_actions: 'Next Actions' }; -function normalizeTicker(ticker: string) { - return ticker.trim().toUpperCase(); -} - -function normalizeOptionalString(value?: string | null) { - const normalized = value?.trim(); - return normalized ? normalized : null; -} - -function normalizePositiveInteger(value?: number | null) { - if (value === null || value === undefined || !Number.isFinite(value)) { - return null; - } - - const normalized = Math.trunc(value); - return normalized > 0 ? normalized : null; -} - -function normalizeRecord(value?: Record | null) { - if (!value || typeof value !== 'object' || Array.isArray(value)) { - return null; - } - - return value; -} - -function normalizeTags(tags?: string[] | null) { - if (!Array.isArray(tags)) { - return []; - } - - const unique = new Set(); - - for (const entry of tags) { - if (typeof entry !== 'string') { - continue; - } - - const normalized = entry.trim(); - if (!normalized) { - continue; - } - - unique.add(normalized); - } - - return [...unique]; -} - function buildArtifactSearchText(input: { title?: string | null; summary?: string | null; @@ -409,7 +368,7 @@ export async function createResearchArtifactRecord(input: { throw new Error('ticker is required'); } - const now = new Date().toISOString(); + const now = nowIso(); const title = normalizeOptionalString(input.title); const summary = normalizeOptionalString(input.summary); const bodyMarkdown = normalizeOptionalString(input.bodyMarkdown); @@ -520,7 +479,7 @@ export async function upsertSystemResearchArtifact(input: { search_text: searchText, tags: normalizeTags(input.tags), metadata: normalizeRecord(input.metadata), - updated_at: new Date().toISOString() + updated_at: nowIso() }) .where(eq(researchArtifact.id, existing.id)) .returning(); @@ -566,7 +525,7 @@ export async function updateResearchArtifactRecord(input: { metadata, tags, search_text: searchText, - updated_at: new Date().toISOString() + updated_at: nowIso() }) .where(eq(researchArtifact.id, input.id)) .returning(); @@ -702,7 +661,7 @@ export async function upsertResearchMemoRecord(input: { throw new Error('ticker is required'); } - const now = new Date().toISOString(); + const now = nowIso(); const existing = await getResearchMemoByTicker(input.userId, ticker); if (!existing) { @@ -796,7 +755,7 @@ export async function addResearchMemoEvidenceLink(input: { .then((rows) => (rows[0]?.maxOrder ?? 0) + 1) : Math.max(0, Math.trunc(input.sortOrder)); - const now = new Date().toISOString(); + const now = nowIso(); if (existing.length > 0) { await db .update(researchMemoEvidence) @@ -894,7 +853,7 @@ export async function getResearchPacket(userId: string, ticker: string): Promise return { ticker: normalizedTicker, companyName: coverage?.company_name ?? latestFiling?.company_name ?? null, - generated_at: new Date().toISOString(), + generated_at: nowIso(), memo, sections: toPacketSections(memo, evidenceBySection) }; diff --git a/lib/server/repos/tasks.ts b/lib/server/repos/tasks.ts index 438c220..8e366c1 100644 --- a/lib/server/repos/tasks.ts +++ b/lib/server/repos/tasks.ts @@ -3,6 +3,7 @@ import type { Task, TaskStage, TaskStageContext, TaskStageEvent, TaskStatus, Tas import { db } from '@/lib/server/db'; import { taskRun, taskStageEvent } from '@/lib/server/db/schema'; import { buildTaskNotification } from '@/lib/server/task-notifications'; +import { nowIso } from '@/lib/server/utils'; type TaskRow = typeof taskRun.$inferSelect; type TaskStageEventRow = typeof taskStageEvent.$inferSelect; @@ -110,7 +111,7 @@ async function insertTaskStageEvent(executor: InsertExecutor, input: EventInsert } export async function createTaskRunRecord(input: CreateTaskInput) { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const [row] = await tx @@ -219,7 +220,7 @@ async function attemptAtomicInsert( } export async function createTaskRunRecordAtomic(input: CreateTaskInput): Promise { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const result = await attemptAtomicInsert(tx, input, now); @@ -235,7 +236,7 @@ export async function setTaskWorkflowRunId(taskId: string, workflowRunId: string .update(taskRun) .set({ workflow_run_id: workflowRunId, - updated_at: new Date().toISOString() + updated_at: nowIso() }) .where(eq(taskRun.id, taskId)); } @@ -313,7 +314,7 @@ export async function findInFlightTaskByResourceKey( } export async function markTaskRunning(taskId: string) { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const [row] = await tx @@ -354,7 +355,7 @@ export async function updateTaskStage( detail: string | null = null, context: TaskStageContext | null = null ) { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const [current] = await tx @@ -403,7 +404,7 @@ export async function completeTask( result: Record, completion: TaskCompletionState = {} ) { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const [row] = await tx @@ -445,7 +446,7 @@ export async function markTaskFailure( stage: TaskStage = 'failed', failure: TaskCompletionState = {} ) { - const now = new Date().toISOString(); + const now = nowIso(); return await db.transaction(async (tx) => { const [row] = await tx @@ -513,7 +514,7 @@ export async function setTaskStatusFromWorkflow( return toTask(current); } - const now = new Date().toISOString(); + const now = nowIso(); const [row] = await tx .update(taskRun) .set({ @@ -551,7 +552,7 @@ export async function updateTaskNotificationState( userId: string, input: UpdateTaskNotificationStateInput ) { - const now = new Date().toISOString(); + const now = nowIso(); const patch: Partial = { updated_at: now }; diff --git a/lib/server/repos/watchlist.ts b/lib/server/repos/watchlist.ts index f718e28..45fe273 100644 --- a/lib/server/repos/watchlist.ts +++ b/lib/server/repos/watchlist.ts @@ -6,38 +6,12 @@ import type { } from '@/lib/types'; import { db } from '@/lib/server/db'; import { watchlistItem } from '@/lib/server/db/schema'; +import { normalizeTicker, normalizeTagsOrNull, nowIso } from '@/lib/server/utils'; type WatchlistRow = typeof watchlistItem.$inferSelect; const DEFAULT_STATUS: CoverageStatus = 'backlog'; const DEFAULT_PRIORITY: CoveragePriority = 'medium'; -function normalizeTags(tags?: string[]) { - if (!Array.isArray(tags)) { - return null; - } - - const unique = new Set(); - - for (const entry of tags) { - if (typeof entry !== 'string') { - continue; - } - - const tag = entry.trim(); - if (!tag) { - continue; - } - - unique.add(tag); - } - - if (unique.size === 0) { - return null; - } - - return [...unique]; -} - function toWatchlistItem(row: WatchlistRow, latestFilingDate: string | null = null): WatchlistItem { return { id: row.id, @@ -79,7 +53,7 @@ export async function getWatchlistItemById(userId: string, id: number) { } export async function getWatchlistItemByTicker(userId: string, ticker: string) { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } @@ -104,15 +78,15 @@ export async function upsertWatchlistItemRecord(input: { priority?: CoveragePriority; lastReviewedAt?: string | null; }) { - const normalizedTicker = input.ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(input.ticker); const normalizedSector = input.sector?.trim() ? input.sector.trim() : null; const normalizedCategory = input.category?.trim() ? input.category.trim() : null; - const normalizedTags = normalizeTags(input.tags); + const normalizedTags = normalizeTagsOrNull(input.tags); const normalizedCompanyName = input.companyName.trim(); const normalizedLastReviewedAt = input.lastReviewedAt?.trim() ? input.lastReviewedAt.trim() : null; - const now = new Date().toISOString(); + const timestamp = nowIso(); const [inserted] = await db .insert(watchlistItem) @@ -125,8 +99,8 @@ export async function upsertWatchlistItemRecord(input: { tags: normalizedTags, status: input.status ?? DEFAULT_STATUS, priority: input.priority ?? DEFAULT_PRIORITY, - created_at: now, - updated_at: now, + created_at: timestamp, + updated_at: timestamp, last_reviewed_at: normalizedLastReviewedAt }) .onConflictDoNothing({ @@ -156,7 +130,7 @@ export async function upsertWatchlistItemRecord(input: { tags: normalizedTags, status: input.status ?? existing?.status ?? DEFAULT_STATUS, priority: input.priority ?? existing?.priority ?? DEFAULT_PRIORITY, - updated_at: now, + updated_at: timestamp, last_reviewed_at: normalizedLastReviewedAt ?? existing?.last_reviewed_at ?? null }) .where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, normalizedTicker))) @@ -212,7 +186,7 @@ export async function updateWatchlistItemRecord(input: { : null; const nextTags = input.tags === undefined ? existing.tags ?? null - : normalizeTags(input.tags); + : normalizeTagsOrNull(input.tags); const nextLastReviewedAt = input.lastReviewedAt === undefined ? existing.last_reviewed_at : input.lastReviewedAt?.trim() @@ -228,7 +202,7 @@ export async function updateWatchlistItemRecord(input: { tags: nextTags, status: input.status ?? existing.status ?? DEFAULT_STATUS, priority: input.priority ?? existing.priority ?? DEFAULT_PRIORITY, - updated_at: new Date().toISOString(), + updated_at: nowIso(), last_reviewed_at: nextLastReviewedAt }) .where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.id, input.id))) @@ -242,7 +216,7 @@ export async function updateWatchlistReviewByTicker( ticker: string, reviewedAt: string ) { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } diff --git a/lib/server/search.ts b/lib/server/search.ts index c2d327b..f6d198d 100644 --- a/lib/server/search.ts +++ b/lib/server/search.ts @@ -17,6 +17,7 @@ import { listResearchJournalEntries, listResearchJournalEntriesForUser } from '@/lib/server/repos/research-journal'; +import { normalizeTicker } from '@/lib/server/utils'; type SearchDocumentScope = 'global' | 'user'; type SearchDocumentSourceKind = 'filing_document' | 'filing_brief' | 'research_note'; @@ -131,11 +132,6 @@ function escapeLike(value: string) { return value.replace(/[%_]/g, (match) => `\\${match}`); } -function normalizeTicker(value: string | null | undefined) { - const normalized = value?.trim().toUpperCase() ?? ''; - return normalized.length > 0 ? normalized : null; -} - function normalizeSearchSources(sources?: SearchSource[]) { const normalized = new Set(); diff --git a/lib/server/sec-company-profile.ts b/lib/server/sec-company-profile.ts index eaee1ba..b6345d1 100644 --- a/lib/server/sec-company-profile.ts +++ b/lib/server/sec-company-profile.ts @@ -1,4 +1,5 @@ import type { CompanyProfile, CompanyValuationSnapshot } from '@/lib/types'; +import { normalizeTicker } from '@/lib/server/utils'; type FetchImpl = typeof fetch; @@ -261,7 +262,7 @@ export async function getSecCompanyProfile( ticker: string, options?: { fetchImpl?: FetchImpl } ): Promise { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } diff --git a/lib/server/task-processors.ts b/lib/server/task-processors.ts index bef9308..a11dffb 100644 --- a/lib/server/task-processors.ts +++ b/lib/server/task-processors.ts @@ -39,6 +39,7 @@ import { } from '@/lib/server/sec'; import { enqueueTask } from '@/lib/server/tasks'; import { hydrateFilingTaxonomySnapshot } from '@/lib/server/taxonomy/engine'; +import { nowIso } from '@/lib/server/utils'; const EXTRACTION_REQUIRED_KEYS = [ 'summary', @@ -762,7 +763,7 @@ async function processSyncFilings(task: Task) { await deleteCompanyFinancialBundlesForTicker(filing.ticker); taxonomySnapshotsHydrated += 1; } catch (error) { - const now = new Date().toISOString(); + const now = nowIso(); await upsertFilingTaxonomySnapshot({ filing_id: filing.id, ticker: filing.ticker, @@ -961,7 +962,7 @@ async function processRefreshPrices(task: Task) { } } ); - const updatedCount = await applyRefreshedPrices(userId, quotes, new Date().toISOString()); + const updatedCount = await applyRefreshedPrices(userId, quotes, nowIso()); const result = { updatedCount, diff --git a/lib/server/utils/index.ts b/lib/server/utils/index.ts new file mode 100644 index 0000000..1218640 --- /dev/null +++ b/lib/server/utils/index.ts @@ -0,0 +1,20 @@ +export { + normalizeTicker, + normalizeTickerOrNull, + normalizeTags, + normalizeTagsOrNull, + normalizeOptionalString, + normalizeRecord, + normalizePositiveInteger, + nowIso, + todayIso +} from './normalize'; + +export { + asRecord, + asOptionalRecord, + asPositiveNumber, + asBoolean, + asStringArray, + asEnum +} from './validation'; diff --git a/lib/server/utils/normalize.ts b/lib/server/utils/normalize.ts new file mode 100644 index 0000000..ac6c607 --- /dev/null +++ b/lib/server/utils/normalize.ts @@ -0,0 +1,51 @@ +export function normalizeTicker(ticker: string): string { + return ticker.trim().toUpperCase(); +} + +export function normalizeTickerOrNull(ticker: unknown): string | null { + if (typeof ticker !== 'string') return null; + const normalized = ticker.trim().toUpperCase(); + return normalized || null; +} + +export function normalizeTags(tags?: unknown): string[] { + if (!Array.isArray(tags)) return []; + + const unique = new Set(); + for (const entry of tags) { + if (typeof entry !== 'string') continue; + const tag = entry.trim(); + if (tag) unique.add(tag); + } + return [...unique]; +} + +export function normalizeTagsOrNull(tags?: unknown): string[] | null { + const result = normalizeTags(tags); + return result.length > 0 ? result : null; +} + +export function normalizeOptionalString(value?: unknown): string | null { + if (typeof value !== 'string') return null; + const normalized = value.trim(); + return normalized || null; +} + +export function normalizeRecord(value?: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return value as Record; +} + +export function normalizePositiveInteger(value?: unknown): number | null { + if (value === null || value === undefined || !Number.isFinite(value as number)) return null; + const normalized = Math.trunc(value as number); + return normalized > 0 ? normalized : null; +} + +export function nowIso(): string { + return new Date().toISOString(); +} + +export function todayIso(): string { + return new Date().toISOString().slice(0, 10); +} diff --git a/lib/server/utils/validation.ts b/lib/server/utils/validation.ts new file mode 100644 index 0000000..08c2d64 --- /dev/null +++ b/lib/server/utils/validation.ts @@ -0,0 +1,56 @@ +export function asRecord(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + return value as Record; +} + +export function asOptionalRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + return value as Record; +} + +export function asPositiveNumber(value: unknown): number | null { + const parsed = typeof value === 'number' ? value : Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +export function asBoolean(value: unknown, fallback = false): boolean { + if (typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (normalized === 'true' || normalized === '1' || normalized === 'yes') { + return true; + } + if (normalized === 'false' || normalized === '0' || normalized === 'no') { + return false; + } + } + + return fallback; +} + +export function asStringArray(value: unknown): string[] { + const source = Array.isArray(value) + ? value + : typeof value === 'string' + ? value.split(',') + : []; + + const unique = new Set(); + for (const entry of source) { + if (typeof entry !== 'string') continue; + const tag = entry.trim(); + if (tag) unique.add(tag); + } + return [...unique]; +} + +export function asEnum(value: unknown, allowed: readonly T[]): T | undefined { + return allowed.includes(value as T) ? (value as T) : undefined; +} diff --git a/lib/server/yahoo-company-profile.ts b/lib/server/yahoo-company-profile.ts index 944de1a..44e96d8 100644 --- a/lib/server/yahoo-company-profile.ts +++ b/lib/server/yahoo-company-profile.ts @@ -1,3 +1,5 @@ +import { normalizeTicker } from '@/lib/server/utils'; + type FetchImpl = typeof fetch; type CacheEntry = { @@ -142,7 +144,7 @@ export async function getYahooCompanyDescription( ticker: string, options?: { fetchImpl?: FetchImpl } ) { - const normalizedTicker = ticker.trim().toUpperCase(); + const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; }