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,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,
|
||||
|
||||
Reference in New Issue
Block a user