From b55fbf094272e6361eaedc383f2adb309e5edfdd Mon Sep 17 00:00:00 2001 From: francy51 Date: Sun, 1 Mar 2026 18:55:59 -0500 Subject: [PATCH] Auto-queue filings sync on new ticker inserts --- lib/server/api/app.ts | 36 +++++++++++++++++++++++++---- lib/server/repos/holdings.ts | 10 ++++++-- lib/server/repos/watchlist.ts | 43 +++++++++++++++++++++++++++-------- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/lib/server/api/app.ts b/lib/server/api/app.ts index 3fa64f0..e0e40b0 100644 --- a/lib/server/api/app.ts +++ b/lib/server/api/app.ts @@ -28,6 +28,7 @@ import { const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed']; const FINANCIAL_FORMS: ReadonlySet = new Set(['10-K', '10-Q']); +const AUTO_FILING_SYNC_LIMIT = 20; function asRecord(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { @@ -53,6 +54,25 @@ function withFinancialMetricsPolicy(filing: Filing): Filing { }; } +async function queueAutoFilingSync(userId: string, ticker: string) { + try { + await enqueueTask({ + userId, + taskType: 'sync_filings', + payload: { + ticker, + limit: AUTO_FILING_SYNC_LIMIT + }, + priority: 90 + }); + + return true; + } catch (error) { + console.error(`[auto-filing-sync] failed for ${ticker}:`, error); + return false; + } +} + const authHandler = ({ request }: { request: Request }) => auth.handler(request); export const app = new Elysia({ prefix: '/api' }) @@ -112,14 +132,18 @@ export const app = new Elysia({ prefix: '/api' }) } try { - const item = await upsertWatchlistItemRecord({ + const { item, created } = await upsertWatchlistItemRecord({ userId: session.user.id, ticker, companyName, sector }); - return Response.json({ item }); + const autoFilingSyncQueued = created + ? await queueAutoFilingSync(session.user.id, ticker) + : false; + + return Response.json({ item, autoFilingSyncQueued }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to create watchlist item')); } @@ -189,7 +213,7 @@ export const app = new Elysia({ prefix: '/api' }) try { const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost; - const holding = await upsertHoldingRecord({ + const { holding, created } = await upsertHoldingRecord({ userId: session.user.id, ticker, shares, @@ -197,7 +221,11 @@ export const app = new Elysia({ prefix: '/api' }) currentPrice }); - return Response.json({ holding }); + const autoFilingSyncQueued = created + ? await queueAutoFilingSync(session.user.id, ticker) + : false; + + return Response.json({ holding, autoFilingSyncQueued }); } catch (error) { return jsonError(asErrorMessage(error, 'Failed to save holding')); } diff --git a/lib/server/repos/holdings.ts b/lib/server/repos/holdings.ts index 4467081..e2f4afe 100644 --- a/lib/server/repos/holdings.ts +++ b/lib/server/repos/holdings.ts @@ -96,7 +96,10 @@ export async function upsertHoldingRecord(input: { .where(eq(holding.id, existing.id)) .returning(); - return toHolding(updated); + return { + holding: toHolding(updated), + created: false + }; } const normalized = normalizeHoldingInput({ @@ -140,7 +143,10 @@ export async function upsertHoldingRecord(input: { }) .returning(); - return toHolding(inserted); + return { + holding: toHolding(inserted), + created: true + }; } export async function updateHoldingByIdRecord(input: { diff --git a/lib/server/repos/watchlist.ts b/lib/server/repos/watchlist.ts index f638d67..9a47156 100644 --- a/lib/server/repos/watchlist.ts +++ b/lib/server/repos/watchlist.ts @@ -32,25 +32,48 @@ export async function upsertWatchlistItemRecord(input: { companyName: string; sector?: string; }) { - const [row] = await db + const normalizedTicker = input.ticker.trim().toUpperCase(); + const normalizedSector = input.sector?.trim() ? input.sector.trim() : null; + const now = new Date().toISOString(); + + const [inserted] = await db .insert(watchlistItem) .values({ user_id: input.userId, - ticker: input.ticker, + ticker: normalizedTicker, company_name: input.companyName, - sector: input.sector?.trim() ? input.sector.trim() : null, - created_at: new Date().toISOString() + sector: normalizedSector, + created_at: now }) - .onConflictDoUpdate({ + .onConflictDoNothing({ target: [watchlistItem.user_id, watchlistItem.ticker], - set: { - company_name: input.companyName, - sector: input.sector?.trim() ? input.sector.trim() : null - } }) .returning(); - return toWatchlistItem(row); + if (inserted) { + return { + item: toWatchlistItem(inserted), + created: true + }; + } + + const [updated] = await db + .update(watchlistItem) + .set({ + company_name: input.companyName, + sector: normalizedSector + }) + .where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, normalizedTicker))) + .returning(); + + if (!updated) { + throw new Error(`Watchlist item ${normalizedTicker} was not found after upsert conflict resolution`); + } + + return { + item: toWatchlistItem(updated), + created: false + }; } export async function deleteWatchlistItemRecord(userId: string, id: number) {