Auto-queue filings sync on new ticker inserts

This commit is contained in:
2026-03-01 18:55:59 -05:00
parent 856af03b39
commit b55fbf0942
3 changed files with 73 additions and 16 deletions

View File

@@ -28,6 +28,7 @@ import {
const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed']; const ALLOWED_STATUSES: TaskStatus[] = ['queued', 'running', 'completed', 'failed'];
const FINANCIAL_FORMS: ReadonlySet<Filing['filing_type']> = new Set(['10-K', '10-Q']); const FINANCIAL_FORMS: ReadonlySet<Filing['filing_type']> = new Set(['10-K', '10-Q']);
const AUTO_FILING_SYNC_LIMIT = 20;
function asRecord(value: unknown): Record<string, unknown> { function asRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) { 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); const authHandler = ({ request }: { request: Request }) => auth.handler(request);
export const app = new Elysia({ prefix: '/api' }) export const app = new Elysia({ prefix: '/api' })
@@ -112,14 +132,18 @@ export const app = new Elysia({ prefix: '/api' })
} }
try { try {
const item = await upsertWatchlistItemRecord({ const { item, created } = await upsertWatchlistItemRecord({
userId: session.user.id, userId: session.user.id,
ticker, ticker,
companyName, companyName,
sector sector
}); });
return Response.json({ item }); const autoFilingSyncQueued = created
? await queueAutoFilingSync(session.user.id, ticker)
: false;
return Response.json({ item, autoFilingSyncQueued });
} catch (error) { } catch (error) {
return jsonError(asErrorMessage(error, 'Failed to create watchlist item')); return jsonError(asErrorMessage(error, 'Failed to create watchlist item'));
} }
@@ -189,7 +213,7 @@ export const app = new Elysia({ prefix: '/api' })
try { try {
const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost; const currentPrice = asPositiveNumber(payload.currentPrice) ?? avgCost;
const holding = await upsertHoldingRecord({ const { holding, created } = await upsertHoldingRecord({
userId: session.user.id, userId: session.user.id,
ticker, ticker,
shares, shares,
@@ -197,7 +221,11 @@ export const app = new Elysia({ prefix: '/api' })
currentPrice currentPrice
}); });
return Response.json({ holding }); const autoFilingSyncQueued = created
? await queueAutoFilingSync(session.user.id, ticker)
: false;
return Response.json({ holding, autoFilingSyncQueued });
} catch (error) { } catch (error) {
return jsonError(asErrorMessage(error, 'Failed to save holding')); return jsonError(asErrorMessage(error, 'Failed to save holding'));
} }

View File

@@ -96,7 +96,10 @@ export async function upsertHoldingRecord(input: {
.where(eq(holding.id, existing.id)) .where(eq(holding.id, existing.id))
.returning(); .returning();
return toHolding(updated); return {
holding: toHolding(updated),
created: false
};
} }
const normalized = normalizeHoldingInput({ const normalized = normalizeHoldingInput({
@@ -140,7 +143,10 @@ export async function upsertHoldingRecord(input: {
}) })
.returning(); .returning();
return toHolding(inserted); return {
holding: toHolding(inserted),
created: true
};
} }
export async function updateHoldingByIdRecord(input: { export async function updateHoldingByIdRecord(input: {

View File

@@ -32,25 +32,48 @@ export async function upsertWatchlistItemRecord(input: {
companyName: string; companyName: string;
sector?: 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) .insert(watchlistItem)
.values({ .values({
user_id: input.userId, user_id: input.userId,
ticker: input.ticker, ticker: normalizedTicker,
company_name: input.companyName, company_name: input.companyName,
sector: input.sector?.trim() ? input.sector.trim() : null, sector: normalizedSector,
created_at: new Date().toISOString() created_at: now
}) })
.onConflictDoUpdate({ .onConflictDoNothing({
target: [watchlistItem.user_id, watchlistItem.ticker], target: [watchlistItem.user_id, watchlistItem.ticker],
set: {
company_name: input.companyName,
sector: input.sector?.trim() ? input.sector.trim() : null
}
}) })
.returning(); .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) { export async function deleteWatchlistItemRecord(userId: string, id: number) {