Auto-queue filings sync on new ticker inserts
This commit is contained in:
@@ -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'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user