Implement fiscal-style research MVP flows
Some checks failed
PR Checks / typecheck-and-build (push) Has been cancelled
Some checks failed
PR Checks / typecheck-and-build (push) Has been cancelled
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
import type { WatchlistItem } from '@/lib/types';
|
||||
import type {
|
||||
CoveragePriority,
|
||||
CoverageStatus,
|
||||
WatchlistItem
|
||||
} from '@/lib/types';
|
||||
import { db } from '@/lib/server/db';
|
||||
import { watchlistItem } from '@/lib/server/db/schema';
|
||||
|
||||
type WatchlistRow = typeof watchlistItem.$inferSelect;
|
||||
const DEFAULT_STATUS: CoverageStatus = 'backlog';
|
||||
const DEFAULT_PRIORITY: CoveragePriority = 'medium';
|
||||
|
||||
function normalizeTags(tags?: string[]) {
|
||||
if (!Array.isArray(tags)) {
|
||||
@@ -32,7 +38,7 @@ function normalizeTags(tags?: string[]) {
|
||||
return [...unique];
|
||||
}
|
||||
|
||||
function toWatchlistItem(row: WatchlistRow): WatchlistItem {
|
||||
function toWatchlistItem(row: WatchlistRow, latestFilingDate: string | null = null): WatchlistItem {
|
||||
return {
|
||||
id: row.id,
|
||||
user_id: row.user_id,
|
||||
@@ -43,7 +49,12 @@ function toWatchlistItem(row: WatchlistRow): WatchlistItem {
|
||||
tags: Array.isArray(row.tags)
|
||||
? row.tags.filter((entry): entry is string => typeof entry === 'string')
|
||||
: [],
|
||||
created_at: row.created_at
|
||||
created_at: row.created_at,
|
||||
status: row.status ?? DEFAULT_STATUS,
|
||||
priority: row.priority ?? DEFAULT_PRIORITY,
|
||||
updated_at: row.updated_at || row.created_at,
|
||||
last_reviewed_at: row.last_reviewed_at ?? null,
|
||||
latest_filing_date: latestFilingDate
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,9 +63,19 @@ export async function listWatchlistItems(userId: string) {
|
||||
.select()
|
||||
.from(watchlistItem)
|
||||
.where(eq(watchlistItem.user_id, userId))
|
||||
.orderBy(desc(watchlistItem.created_at));
|
||||
.orderBy(desc(watchlistItem.updated_at), desc(watchlistItem.created_at));
|
||||
|
||||
return rows.map(toWatchlistItem);
|
||||
return rows.map((row) => toWatchlistItem(row));
|
||||
}
|
||||
|
||||
export async function getWatchlistItemById(userId: string, id: number) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(watchlistItem)
|
||||
.where(and(eq(watchlistItem.user_id, userId), eq(watchlistItem.id, id)))
|
||||
.limit(1);
|
||||
|
||||
return row ? toWatchlistItem(row) : null;
|
||||
}
|
||||
|
||||
export async function getWatchlistItemByTicker(userId: string, ticker: string) {
|
||||
@@ -79,11 +100,18 @@ export async function upsertWatchlistItemRecord(input: {
|
||||
sector?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
status?: CoverageStatus;
|
||||
priority?: CoveragePriority;
|
||||
lastReviewedAt?: string | null;
|
||||
}) {
|
||||
const normalizedTicker = input.ticker.trim().toUpperCase();
|
||||
const normalizedSector = input.sector?.trim() ? input.sector.trim() : null;
|
||||
const normalizedCategory = input.category?.trim() ? input.category.trim() : null;
|
||||
const normalizedTags = normalizeTags(input.tags);
|
||||
const normalizedCompanyName = input.companyName.trim();
|
||||
const normalizedLastReviewedAt = input.lastReviewedAt?.trim()
|
||||
? input.lastReviewedAt.trim()
|
||||
: null;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const [inserted] = await db
|
||||
@@ -91,11 +119,15 @@ export async function upsertWatchlistItemRecord(input: {
|
||||
.values({
|
||||
user_id: input.userId,
|
||||
ticker: normalizedTicker,
|
||||
company_name: input.companyName,
|
||||
company_name: normalizedCompanyName,
|
||||
sector: normalizedSector,
|
||||
category: normalizedCategory,
|
||||
tags: normalizedTags,
|
||||
created_at: now
|
||||
status: input.status ?? DEFAULT_STATUS,
|
||||
priority: input.priority ?? DEFAULT_PRIORITY,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_reviewed_at: normalizedLastReviewedAt
|
||||
})
|
||||
.onConflictDoNothing({
|
||||
target: [watchlistItem.user_id, watchlistItem.ticker],
|
||||
@@ -109,13 +141,23 @@ export async function upsertWatchlistItemRecord(input: {
|
||||
};
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(watchlistItem)
|
||||
.where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, normalizedTicker)))
|
||||
.limit(1);
|
||||
|
||||
const [updated] = await db
|
||||
.update(watchlistItem)
|
||||
.set({
|
||||
company_name: input.companyName,
|
||||
company_name: normalizedCompanyName,
|
||||
sector: normalizedSector,
|
||||
category: normalizedCategory,
|
||||
tags: normalizedTags
|
||||
tags: normalizedTags,
|
||||
status: input.status ?? existing?.status ?? DEFAULT_STATUS,
|
||||
priority: input.priority ?? existing?.priority ?? DEFAULT_PRIORITY,
|
||||
updated_at: now,
|
||||
last_reviewed_at: normalizedLastReviewedAt ?? existing?.last_reviewed_at ?? null
|
||||
})
|
||||
.where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, normalizedTicker)))
|
||||
.returning();
|
||||
@@ -130,6 +172,93 @@ export async function upsertWatchlistItemRecord(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateWatchlistItemRecord(input: {
|
||||
userId: string;
|
||||
id: number;
|
||||
companyName?: string;
|
||||
sector?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
status?: CoverageStatus;
|
||||
priority?: CoveragePriority;
|
||||
lastReviewedAt?: string | null;
|
||||
}) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(watchlistItem)
|
||||
.where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.id, input.id)))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextCompanyName = input.companyName === undefined
|
||||
? existing.company_name
|
||||
: input.companyName.trim();
|
||||
if (!nextCompanyName) {
|
||||
throw new Error('companyName is required');
|
||||
}
|
||||
|
||||
const nextSector = input.sector === undefined
|
||||
? existing.sector
|
||||
: input.sector.trim()
|
||||
? input.sector.trim()
|
||||
: null;
|
||||
const nextCategory = input.category === undefined
|
||||
? existing.category
|
||||
: input.category.trim()
|
||||
? input.category.trim()
|
||||
: null;
|
||||
const nextTags = input.tags === undefined
|
||||
? existing.tags ?? null
|
||||
: normalizeTags(input.tags);
|
||||
const nextLastReviewedAt = input.lastReviewedAt === undefined
|
||||
? existing.last_reviewed_at
|
||||
: input.lastReviewedAt?.trim()
|
||||
? input.lastReviewedAt.trim()
|
||||
: null;
|
||||
|
||||
const [updated] = await db
|
||||
.update(watchlistItem)
|
||||
.set({
|
||||
company_name: nextCompanyName,
|
||||
sector: nextSector,
|
||||
category: nextCategory,
|
||||
tags: nextTags,
|
||||
status: input.status ?? existing.status ?? DEFAULT_STATUS,
|
||||
priority: input.priority ?? existing.priority ?? DEFAULT_PRIORITY,
|
||||
updated_at: new Date().toISOString(),
|
||||
last_reviewed_at: nextLastReviewedAt
|
||||
})
|
||||
.where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.id, input.id)))
|
||||
.returning();
|
||||
|
||||
return updated ? toWatchlistItem(updated) : null;
|
||||
}
|
||||
|
||||
export async function updateWatchlistReviewByTicker(
|
||||
userId: string,
|
||||
ticker: string,
|
||||
reviewedAt: string
|
||||
) {
|
||||
const normalizedTicker = ticker.trim().toUpperCase();
|
||||
if (!normalizedTicker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(watchlistItem)
|
||||
.set({
|
||||
last_reviewed_at: reviewedAt,
|
||||
updated_at: reviewedAt
|
||||
})
|
||||
.where(and(eq(watchlistItem.user_id, userId), eq(watchlistItem.ticker, normalizedTicker)))
|
||||
.returning();
|
||||
|
||||
return updated ? toWatchlistItem(updated) : null;
|
||||
}
|
||||
|
||||
export async function deleteWatchlistItemRecord(userId: string, id: number) {
|
||||
const removed = await db
|
||||
.delete(watchlistItem)
|
||||
|
||||
Reference in New Issue
Block a user