Add category and tags granularity to company sync flows

This commit is contained in:
2026-03-03 23:10:08 -05:00
parent 717e747869
commit 610fce8db3
12 changed files with 415 additions and 29 deletions

View File

@@ -26,6 +26,7 @@ import {
import { getLatestPortfolioInsight } from '@/lib/server/repos/insights';
import {
deleteWatchlistItemRecord,
getWatchlistItemByTicker,
listWatchlistItems,
upsertWatchlistItemRecord
} from '@/lib/server/repos/watchlist';
@@ -86,6 +87,39 @@ function asBoolean(value: unknown, fallback = false) {
return fallback;
}
function asOptionalString(value: unknown) {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function asTags(value: unknown) {
const source = Array.isArray(value)
? value
: typeof value === 'string'
? value.split(',')
: [];
const unique = new Set<string>();
for (const entry of source) {
if (typeof entry !== 'string') {
continue;
}
const tag = entry.trim();
if (!tag) {
continue;
}
unique.add(tag);
}
return [...unique];
}
function asStatementMode(value: unknown): FinancialStatementMode {
return FINANCIAL_STATEMENT_MODES.includes(value as FinancialStatementMode)
? value as FinancialStatementMode
@@ -115,15 +149,38 @@ function withFinancialMetricsPolicy(filing: Filing): Filing {
};
}
async function queueAutoFilingSync(userId: string, ticker: string) {
function buildSyncFilingsPayload(input: {
ticker: string;
limit: number;
category?: unknown;
tags?: unknown;
}) {
const category = asOptionalString(input.category);
const tags = asTags(input.tags);
return {
ticker: input.ticker,
limit: input.limit,
...(category ? { category } : {}),
...(tags.length > 0 ? { tags } : {})
};
}
async function queueAutoFilingSync(
userId: string,
ticker: string,
metadata?: { category?: unknown; tags?: unknown }
) {
try {
await enqueueTask({
userId,
taskType: 'sync_filings',
payload: {
payload: buildSyncFilingsPayload({
ticker,
limit: AUTO_FILING_SYNC_LIMIT
},
limit: AUTO_FILING_SYNC_LIMIT,
category: metadata?.category,
tags: metadata?.tags
}),
priority: 90,
resourceKey: `sync_filings:${ticker}`
});
@@ -228,7 +285,9 @@ export const app = new Elysia({ prefix: '/api' })
const payload = asRecord(body);
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
const companyName = typeof payload.companyName === 'string' ? payload.companyName.trim() : '';
const sector = typeof payload.sector === 'string' ? payload.sector.trim() : '';
const sector = asOptionalString(payload.sector) ?? '';
const category = asOptionalString(payload.category) ?? '';
const tags = asTags(payload.tags);
if (!ticker) {
return jsonError('ticker is required');
@@ -243,11 +302,16 @@ export const app = new Elysia({ prefix: '/api' })
userId: session.user.id,
ticker,
companyName,
sector
sector,
category,
tags
});
const autoFilingSyncQueued = created
? await queueAutoFilingSync(session.user.id, ticker)
? await queueAutoFilingSync(session.user.id, ticker, {
category: item.category,
tags: item.tags
})
: false;
return Response.json({ item, autoFilingSyncQueued });
@@ -258,7 +322,9 @@ export const app = new Elysia({ prefix: '/api' })
body: t.Object({
ticker: t.String({ minLength: 1 }),
companyName: t.String({ minLength: 1 }),
sector: t.Optional(t.String())
sector: t.Optional(t.String()),
category: t.Optional(t.String()),
tags: t.Optional(t.Union([t.Array(t.String()), t.String()]))
})
})
.delete('/watchlist/:id', async ({ params }) => {
@@ -524,6 +590,8 @@ export const app = new Elysia({ prefix: '/api' })
ticker,
companyName,
sector: watchlistItem?.sector ?? null,
category: watchlistItem?.category ?? null,
tags: watchlistItem?.tags ?? [],
cik: latestFiling?.cik ?? null
},
quote: liveQuote,
@@ -588,13 +656,16 @@ export const app = new Elysia({ prefix: '/api' })
if (shouldQueueSync) {
try {
const watchlistItem = await getWatchlistItemByTicker(session.user.id, ticker);
await enqueueTask({
userId: session.user.id,
taskType: 'sync_filings',
payload: {
payload: buildSyncFilingsPayload({
ticker,
limit: defaultFinancialSyncLimit(window)
},
limit: defaultFinancialSyncLimit(window),
category: watchlistItem?.category,
tags: watchlistItem?.tags
}),
priority: 88,
resourceKey: `sync_filings:${ticker}`
});
@@ -707,6 +778,8 @@ export const app = new Elysia({ prefix: '/api' })
const payload = asRecord(body);
const ticker = typeof payload.ticker === 'string' ? payload.ticker.trim().toUpperCase() : '';
const category = asOptionalString(payload.category);
const tags = asTags(payload.tags);
if (!ticker) {
return jsonError('ticker is required');
@@ -717,10 +790,12 @@ export const app = new Elysia({ prefix: '/api' })
const task = await enqueueTask({
userId: session.user.id,
taskType: 'sync_filings',
payload: {
payload: buildSyncFilingsPayload({
ticker,
limit: Number.isFinite(limit) ? limit : 20
},
limit: Number.isFinite(limit) ? limit : 20,
category,
tags
}),
priority: 90,
resourceKey: `sync_filings:${ticker}`
});
@@ -732,7 +807,9 @@ export const app = new Elysia({ prefix: '/api' })
}, {
body: t.Object({
ticker: t.String({ minLength: 1 }),
limit: t.Optional(t.Numeric())
limit: t.Optional(t.Numeric()),
category: t.Optional(t.String()),
tags: t.Optional(t.Union([t.Array(t.String()), t.String()]))
})
})
.post('/filings/:accessionNumber/analyze', async ({ params }) => {