Implement fiscal-style research MVP flows
Some checks failed
PR Checks / typecheck-and-build (push) Has been cancelled

This commit is contained in:
2026-03-07 09:51:18 -05:00
parent f69e5b671b
commit 52136271d3
26 changed files with 2719 additions and 243 deletions

View File

@@ -1,8 +1,8 @@
import { and, eq } from 'drizzle-orm';
import { and, desc, eq } from 'drizzle-orm';
import type { Holding } from '@/lib/types';
import { recalculateHolding } from '@/lib/server/portfolio';
import { db } from '@/lib/server/db';
import { holding } from '@/lib/server/db/schema';
import { filing, holding, watchlistItem } from '@/lib/server/db/schema';
type HoldingRow = typeof holding.$inferSelect;
@@ -11,6 +11,7 @@ function toHolding(row: HoldingRow): Holding {
id: row.id,
user_id: row.user_id,
ticker: row.ticker,
company_name: row.company_name ?? null,
shares: row.shares,
avg_cost: row.avg_cost,
current_price: row.current_price,
@@ -36,6 +37,41 @@ function normalizeHoldingInput(input: { ticker: string; shares: number; avgCost:
};
}
async function resolveHoldingCompanyName(input: {
userId: string;
ticker: string;
companyName?: string;
existingCompanyName?: string | null;
}) {
const explicitCompanyName = input.companyName?.trim();
if (explicitCompanyName) {
return explicitCompanyName;
}
if (input.existingCompanyName?.trim()) {
return input.existingCompanyName.trim();
}
const [watchlistMatch] = await db
.select({ company_name: watchlistItem.company_name })
.from(watchlistItem)
.where(and(eq(watchlistItem.user_id, input.userId), eq(watchlistItem.ticker, input.ticker)))
.limit(1);
if (watchlistMatch?.company_name?.trim()) {
return watchlistMatch.company_name.trim();
}
const [filingMatch] = await db
.select({ company_name: filing.company_name })
.from(filing)
.where(eq(filing.ticker, input.ticker))
.orderBy(desc(filing.filing_date), desc(filing.updated_at))
.limit(1);
return filingMatch?.company_name?.trim() ? filingMatch.company_name.trim() : null;
}
export async function listUserHoldings(userId: string) {
const rows = await db
.select()
@@ -45,12 +81,28 @@ export async function listUserHoldings(userId: string) {
return sortByMarketValueDesc(rows.map(toHolding));
}
export async function getHoldingByTicker(userId: string, ticker: string) {
const normalizedTicker = ticker.trim().toUpperCase();
if (!normalizedTicker) {
return null;
}
const [row] = await db
.select()
.from(holding)
.where(and(eq(holding.user_id, userId), eq(holding.ticker, normalizedTicker)))
.limit(1);
return row ? toHolding(row) : null;
}
export async function upsertHoldingRecord(input: {
userId: string;
ticker: string;
shares: number;
avgCost: number;
currentPrice?: number;
companyName?: string;
}) {
const ticker = input.ticker.trim().toUpperCase();
const now = new Date().toISOString();
@@ -64,6 +116,12 @@ export async function upsertHoldingRecord(input: {
const currentPrice = Number.isFinite(input.currentPrice)
? Number(input.currentPrice)
: input.avgCost;
const companyName = await resolveHoldingCompanyName({
userId: input.userId,
ticker,
companyName: input.companyName,
existingCompanyName: existing?.company_name ?? null
});
if (existing) {
const normalized = normalizeHoldingInput({
@@ -84,6 +142,7 @@ export async function upsertHoldingRecord(input: {
.update(holding)
.set({
ticker: next.ticker,
company_name: companyName,
shares: next.shares,
avg_cost: next.avg_cost,
current_price: next.current_price,
@@ -113,6 +172,7 @@ export async function upsertHoldingRecord(input: {
id: 0,
user_id: input.userId,
ticker: normalized.ticker,
company_name: companyName,
shares: normalized.shares,
avg_cost: normalized.avg_cost,
current_price: normalized.current_price,
@@ -131,6 +191,7 @@ export async function upsertHoldingRecord(input: {
.values({
user_id: created.user_id,
ticker: created.ticker,
company_name: created.company_name,
shares: created.shares,
avg_cost: created.avg_cost,
current_price: created.current_price,
@@ -155,6 +216,7 @@ export async function updateHoldingByIdRecord(input: {
shares?: number;
avgCost?: number;
currentPrice?: number;
companyName?: string;
}) {
const [existing] = await db
.select()
@@ -176,9 +238,16 @@ export async function updateHoldingByIdRecord(input: {
const currentPrice = Number.isFinite(input.currentPrice)
? Number(input.currentPrice)
: Number(current.current_price ?? current.avg_cost);
const companyName = await resolveHoldingCompanyName({
userId: input.userId,
ticker: current.ticker,
companyName: input.companyName,
existingCompanyName: current.company_name
});
const next = recalculateHolding({
...current,
company_name: companyName,
shares: shares.toFixed(6),
avg_cost: avgCost.toFixed(6),
current_price: currentPrice.toFixed(6),
@@ -189,6 +258,7 @@ export async function updateHoldingByIdRecord(input: {
const [updated] = await db
.update(holding)
.set({
company_name: companyName,
shares: next.shares,
avg_cost: next.avg_cost,
current_price: next.current_price,