Function bodies 201 total
useTheme function · typescript · L25-L51 (27 LOC)src/hooks/use-theme.ts
export function useTheme() {
const [theme, setThemeState] = useState<Theme>("system");
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
const initial: Theme =
stored === "light" || stored === "dark" || stored === "system"
? stored
: "system";
setThemeState(initial);
if (initial === "system") {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => applyTheme("system");
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}
}, []);
const setTheme = useCallback((next: Theme) => {
localStorage.setItem(STORAGE_KEY, next);
applyTheme(next);
setThemeState(next);
}, []);
return { theme, setTheme };
}toast function · typescript · L21-L26 (6 LOC)src/hooks/use-toast.ts
export function toast(opts: Omit<ToastItem, "id">) {
const id = Math.random().toString(36).slice(2);
_state = [{ id, ...opts }, ..._state].slice(0, 5);
notify();
setTimeout(() => dismiss(id), 5000);
}useToasts function · typescript · L33-L45 (13 LOC)src/hooks/use-toast.ts
export function useToasts() {
const [toasts, setToasts] = useState<ToastItem[]>(_state);
useEffect(() => {
const listener = () => setToasts([..._state]);
_listeners.add(listener);
return () => {
_listeners.delete(listener);
};
}, []);
return { toasts, dismiss };
}tooManyRequests function · typescript · L32-L38 (7 LOC)src/lib/api-utils.ts
export function tooManyRequests(error = "Too many requests. Please try again later.", retryAfterMs?: number) {
const headers: Record<string, string> = {};
if (retryAfterMs !== undefined) {
headers["Retry-After"] = String(Math.ceil(retryAfterMs / 1000));
}
return NextResponse.json({ error }, { status: 429, headers });
}sendVerificationEmail function · typescript · L6-L36 (31 LOC)src/lib/email.ts
export async function sendVerificationEmail(
to: string,
code: string
): Promise<void> {
const resend = new Resend(env.RESEND_API_KEY);
const { error } = await resend.emails.send({
from: FROM,
to,
subject: `${code} — your Portfolio Tracker verification code`,
html: `
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px 24px">
<h2 style="margin:0 0 8px;font-size:20px;color:#111">Verify your email</h2>
<p style="margin:0 0 24px;color:#555;font-size:14px">
Enter the code below to complete your Portfolio Tracker sign-up.
It expires in <strong>10 minutes</strong>.
</p>
<div style="display:inline-block;background:#f4f4f5;border-radius:8px;padding:16px 32px;
font-size:32px;font-weight:700;letter-spacing:8px;color:#111;text-align:center">
${code}
</div>
<p style="margin:24px 0 0;color:#999;font-size:12px">
If you did not request thissendPasswordResetEmail function · typescript · L38-L71 (34 LOC)src/lib/email.ts
export async function sendPasswordResetEmail(
to: string,
resetUrl: string
): Promise<void> {
const resend = new Resend(env.RESEND_API_KEY);
const { error } = await resend.emails.send({
from: FROM,
to,
subject: "Reset your Portfolio Tracker password",
html: `
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:32px 24px">
<h2 style="margin:0 0 8px;font-size:20px;color:#111">Reset your password</h2>
<p style="margin:0 0 24px;color:#555;font-size:14px">
We received a request to reset the password for your Portfolio Tracker
account. Click the button below to choose a new password.
This link expires in <strong>1 hour</strong>.
</p>
<a href="${resetUrl}"
style="display:inline-block;background:#111;color:#fff;border-radius:8px;
padding:12px 32px;font-size:14px;font-weight:600;text-decoration:none">
Reset Password
</a>
<p stylerequired function · typescript · L6-L15 (10 LOC)src/lib/env.ts
function required(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(
`Missing required environment variable: ${name}. ` +
`Check your .env file or deployment settings.`
);
}
return value;
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
getPrisma function · typescript · L7-L33 (27 LOC)src/lib/prisma.ts
export function getPrisma(): PrismaClient {
if (globalForPrisma.prisma) return globalForPrisma.prisma;
let client: PrismaClient;
if (process.env.TURSO_DATABASE_URL && process.env.TURSO_AUTH_TOKEN) {
// Dynamic requires for optional Turso adapter — not available as static imports
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { createClient } = require("@libsql/client/web");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { PrismaLibSQL } = require("@prisma/adapter-libsql");
const libsql = createClient({
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
const adapter = new PrismaLibSQL(libsql);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client = new PrismaClient({ adapter } as any);
} else {
client = new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
}
rateLimit function · typescript · L42-L58 (17 LOC)src/lib/rate-limit.ts
export function rateLimit(key: string, max: number, windowMs: number): RateLimitResult {
const now = Date.now();
const bucket = store.get(key);
if (!bucket || now > bucket.resetAt) {
const resetAt = now + windowMs;
store.set(key, { count: 1, resetAt });
return { allowed: true, remaining: max - 1, resetAt };
}
if (bucket.count >= max) {
return { allowed: false, remaining: 0, resetAt: bucket.resetAt };
}
bucket.count++;
return { allowed: true, remaining: max - bucket.count, resetAt: bucket.resetAt };
}getClientIp function · typescript · L61-L67 (7 LOC)src/lib/rate-limit.ts
export function getClientIp(req: Request): string {
return (
req.headers.get("x-forwarded-for")?.split(",")[0].trim() ??
req.headers.get("x-real-ip") ??
"unknown"
);
}toTradingViewSymbol function · typescript · L7-L13 (7 LOC)src/lib/tradingview-symbol.ts
export function toTradingViewSymbol(symbol: string, assetType: string): string {
if (assetType === "CRYPTO") {
const base = symbol.replace(/-USD$/i, "").replace(/-/g, "").toUpperCase();
return `${base}USD`;
}
return symbol.toUpperCase();
}getAlertsByUser function · typescript · L5-L11 (7 LOC)src/services/alert.service.ts
export async function getAlertsByUser(userId: string) {
return prisma.priceAlert.findMany({
where: { userId },
include: { asset: { select: { name: true, assetType: true } } },
orderBy: [{ active: "desc" }, { createdAt: "desc" }],
});
}getAlertsBySymbol function · typescript · L13-L18 (6 LOC)src/services/alert.service.ts
export async function getAlertsBySymbol(userId: string, symbol: string) {
return prisma.priceAlert.findMany({
where: { userId, symbol },
orderBy: [{ active: "desc" }, { createdAt: "desc" }],
});
}createAlert function · typescript · L20-L30 (11 LOC)src/services/alert.service.ts
export async function createAlert(userId: string, input: CreateAlertInput) {
return prisma.priceAlert.create({
data: {
userId,
assetId: input.assetId,
symbol: input.symbol,
condition: input.condition,
targetPrice: input.targetPrice,
},
});
}reactivateAlert function · typescript · L36-L41 (6 LOC)src/services/alert.service.ts
export async function reactivateAlert(id: string, userId: string) {
return prisma.priceAlert.updateMany({
where: { id, userId },
data: { active: true, triggeredAt: null },
});
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
checkAndFireAlerts function · typescript · L43-L92 (50 LOC)src/services/alert.service.ts
export async function checkAndFireAlerts(userId: string): Promise<TriggeredAlert[]> {
const activeAlerts = await prisma.priceAlert.findMany({
where: { userId, active: true },
include: { asset: { select: { assetType: true } } },
});
if (activeAlerts.length === 0) return [];
// Filter out alerts whose asset relationship may be broken
const validAlerts = activeAlerts.filter((a) => a.asset != null);
if (validAlerts.length === 0) return [];
const marketSymbols = validAlerts.map((a) =>
toMarketSymbol(a.symbol, a.asset.assetType)
);
const quotes = await getBatchQuotes(marketSymbols);
const triggered: TriggeredAlert[] = [];
const now = new Date();
for (const alert of validAlerts) {
const marketSym = toMarketSymbol(alert.symbol, alert.asset.assetType);
const quote = quotes.get(marketSym);
if (!quote) continue;
const fires =
alert.condition === "ABOVE"
? quote.price >= alert.targetPrice
: quote.price <= alert.targetPgetDashboardData function · typescript · L42-L174 (133 LOC)src/services/dashboard.service.ts
export async function getDashboardData(
userId: string,
currency = "USD"
): Promise<DashboardData> {
const portfolios = await prisma.portfolio.findMany({
where: { userId },
include: {
assets: {
include: { transactions: { orderBy: { date: "asc" } } },
},
},
});
const allAssets = portfolios.flatMap((p) => p.assets);
// ── Collect all transactions before grouping (needed for performance) ────
const allTransactions = allAssets
.flatMap((a) =>
a.transactions.map((t) => ({
date: t.date,
type: t.type as string,
quantity: t.quantity,
pricePerUnit: t.pricePerUnit,
}))
)
.sort((a, b) => a.date.getTime() - b.date.getTime());
// ── Group by symbol across all portfolios ───────────────────────────────
// Dashboard / analytics views show one row per ticker. Multiple lots of the
// same symbol (even across different portfolios) are merged via weighted avg.
const groupMap = new Map<string, buildPerformance function · typescript · L189-L236 (48 LOC)src/services/dashboard.service.ts
function buildPerformance(transactions: TxSlice[]) {
if (transactions.length === 0) {
return { daily: [], weekly: [], monthly: [] };
}
const dayMap = new Map<string, number>();
let running = 0;
for (const tx of transactions) {
const key = toDateStr(tx.date);
if (tx.type === "BUY") running += tx.quantity * tx.pricePerUnit;
else if (tx.type === "SELL") running -= tx.quantity * tx.pricePerUnit;
dayMap.set(key, running);
}
const first = new Date(transactions[0].date);
first.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
const allDays: Array<{ date: string; value: number }> = [];
let lastVal = 0;
const cursor = new Date(first);
while (cursor <= today) {
const key = toDateStr(cursor);
if (dayMap.has(key)) lastVal = dayMap.get(key)!;
allDays.push({ date: key, value: lastVal });
cursor.setDate(cursor.getDate() + 1);
}
const daily = allDays.slice(-90);
const weekly: typeof allDays = [];
forgetDividendData function · typescript · L46-L127 (82 LOC)src/services/dividend.service.ts
export async function getDividendData(userId: string): Promise<DividendData> {
const rawTransactions = await prisma.transaction.findMany({
where: {
type: "DIVIDEND",
asset: { portfolio: { userId } },
},
include: {
asset: {
include: { portfolio: { select: { name: true } } },
},
},
orderBy: { date: "desc" },
});
const transactions: DividendTransaction[] = rawTransactions.map((t) => ({
id: t.id,
date: t.date.toISOString(),
amount: t.quantity * t.pricePerUnit,
quantity: t.quantity,
pricePerUnit: t.pricePerUnit,
fees: t.fees,
notes: t.notes,
assetSymbol: t.asset.symbol,
assetName: t.asset.name,
portfolioName: t.asset.portfolio.name,
}));
const totalDividends = transactions.reduce((sum, t) => sum + t.amount, 0);
const totalCount = transactions.length;
// Monthly breakdown
const monthMap = new Map<string, number>();
for (const t of transactions) {
const month = t.date.slice(0, yfQuote function · typescript · L95-L114 (20 LOC)src/services/marketData.ts
async function yfQuote(symbol: string): Promise<PriceQuote> {
const url = `${YF_BASE}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
const res = await fetch(url, { headers: YF_HEADERS, cache: "no-store" });
if (!res.ok) throw new Error(`Yahoo Finance ${res.status} for ${symbol}`);
const json = (await res.json()) as YFChartResponse;
const r = json.chart?.result?.[0];
if (!r) throw new Error(`No Yahoo Finance data for ${symbol}`);
const { meta } = r;
return {
symbol: meta.symbol,
price: meta.regularMarketPrice,
currency: meta.currency ?? "USD",
change: meta.regularMarketChange ?? 0,
changePercent: (meta.regularMarketChangePercent ?? 0) / 100,
volume: meta.regularMarketVolume,
timestamp: meta.regularMarketTime ?? Math.floor(Date.now() / 1000),
};
}yfHistory function · typescript · L116-L141 (26 LOC)src/services/marketData.ts
async function yfHistory(
symbol: string,
range: HistoryRange
): Promise<HistoricalDataPoint[]> {
const url = `${YF_BASE}/${encodeURIComponent(symbol)}?interval=1d&range=${range}`;
const res = await fetch(url, { headers: YF_HEADERS, cache: "no-store" });
if (!res.ok) throw new Error(`Yahoo Finance ${res.status} for ${symbol}`);
const json = (await res.json()) as YFChartResponse;
const r = json.chart?.result?.[0];
if (!r) throw new Error(`No Yahoo Finance history for ${symbol}`);
const timestamps = r.timestamp ?? [];
const q = r.indicators?.quote?.[0] ?? {};
return timestamps
.map((ts, i) => ({
date: new Date(ts * 1000).toISOString().split("T")[0],
open: q.open?.[i] ?? 0,
high: q.high?.[i] ?? 0,
low: q.low?.[i] ?? 0,
close: q.close?.[i] ?? 0,
volume: q.volume?.[i] ?? 0,
}))
.filter((d) => d.close > 0);
}avQuote function · typescript · L147-L175 (29 LOC)src/services/marketData.ts
async function avQuote(symbol: string): Promise<PriceQuote> {
const key = process.env.ALPHA_VANTAGE_API_KEY;
if (!key) throw new Error("ALPHA_VANTAGE_API_KEY not set");
const url = `${AV_BASE}?function=GLOBAL_QUOTE&symbol=${encodeURIComponent(symbol)}&apikey=${key}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error(`Alpha Vantage ${res.status}`);
const json = await res.json();
const q = json["Global Quote"] as Record<string, string> | undefined;
if (!q?.["05. price"]) throw new Error("Invalid Alpha Vantage quote response");
const price = parseFloat(q["05. price"]);
if (!isFinite(price)) throw new Error("Invalid price data from Alpha Vantage");
const change = parseFloat(q["09. change"] ?? "0");
const changePercent = parseFloat((q["10. change percent"] ?? "0%").replace("%", "")) / 100;
const volume = parseInt(q["06. volume"] ?? "0", 10);
return {
symbol: q["01. symbol"],
price,
currency: "USD",
change: isFiavHistory function · typescript · L177-L201 (25 LOC)src/services/marketData.ts
async function avHistory(symbol: string): Promise<HistoricalDataPoint[]> {
const key = process.env.ALPHA_VANTAGE_API_KEY;
if (!key) throw new Error("ALPHA_VANTAGE_API_KEY not set");
const url = `${AV_BASE}?function=TIME_SERIES_DAILY_ADJUSTED&symbol=${encodeURIComponent(symbol)}&outputsize=compact&apikey=${key}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error(`Alpha Vantage ${res.status}`);
const json = await res.json();
const series = json["Time Series (Daily)"] as
| Record<string, Record<string, string>>
| undefined;
if (!series) throw new Error("Invalid Alpha Vantage history response");
return Object.entries(series)
.map(([date, vals]) => ({
date,
open: parseFloat(vals["1. open"]),
high: parseFloat(vals["2. high"]),
low: parseFloat(vals["3. low"]),
close: parseFloat(vals["4. close"]),
volume: parseInt(vals["6. volume"], 10),
}))
.sort((a, b) => a.date.localeCompare(b.date))All rows above produced by Repobility · https://repobility.com
searchSymbols function · typescript · L231-L247 (17 LOC)src/services/marketData.ts
export async function searchSymbols(query: string): Promise<SymbolSearchResult[]> {
if (!query.trim()) return [];
try {
const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(query)}"esCount=8&newsCount=0&listsCount=0`;
const res = await fetch(url, { headers: YF_HEADERS, cache: "no-store" });
if (!res.ok) return [];
const json = (await res.json()) as YFSearchResponse;
return (json.quotes ?? []).map((q) => ({
symbol: q.symbol,
name: q.longname ?? q.shortname ?? q.symbol,
suggestedType: YF_TYPE_MAP[q.typeDisp ?? ""] ?? "STOCK",
exchange: q.exchange,
}));
} catch {
return [];
}
}getQuote function · typescript · L255-L291 (37 LOC)src/services/marketData.ts
export async function getQuote(
symbol: string
): Promise<MarketDataResult<PriceQuote>> {
const cacheKey = symbol.toUpperCase();
const cached = priceCache.get(cacheKey);
const now = Date.now();
if (cached && now - cached.ts < PRICE_TTL) {
return { data: cached.data, error: null, stale: false };
}
let lastError: string | null = null;
try {
const data = await yfQuote(symbol);
priceCache.set(cacheKey, { data, ts: now });
return { data, error: null, stale: false };
} catch (e) {
lastError = String(e);
}
if (process.env.ALPHA_VANTAGE_API_KEY) {
try {
const data = await avQuote(symbol);
priceCache.set(cacheKey, { data, ts: now });
return { data, error: null, stale: false };
} catch (e) {
lastError = String(e);
}
}
if (cached) {
return { data: cached.data, error: lastError, stale: true };
}
return { data: null, error: lastError, stale: false };
}getBatchQuotes function · typescript · L297-L309 (13 LOC)src/services/marketData.ts
export async function getBatchQuotes(
symbols: string[]
): Promise<Map<string, PriceQuote | null>> {
const unique = [...new Set(symbols.map((s) => s.toUpperCase()))];
const settled = await Promise.allSettled(unique.map((s) => getQuote(s)));
const map = new Map<string, PriceQuote | null>();
unique.forEach((sym, i) => {
const r = settled[i];
map.set(sym, r.status === "fulfilled" ? r.value.data : null);
});
return map;
}getHistoricalData function · typescript · L315-L352 (38 LOC)src/services/marketData.ts
export async function getHistoricalData(
symbol: string,
range: HistoryRange = "1y"
): Promise<MarketDataResult<HistoricalDataPoint[]>> {
const cacheKey = `${symbol.toUpperCase()}:${range}`;
const cached = historyCache.get(cacheKey);
const now = Date.now();
if (cached && now - cached.ts < HISTORY_TTL) {
return { data: cached.data, error: null, stale: false };
}
let lastError: string | null = null;
try {
const data = await yfHistory(symbol, range);
historyCache.set(cacheKey, { data, ts: now });
return { data, error: null, stale: false };
} catch (e) {
lastError = String(e);
}
if (process.env.ALPHA_VANTAGE_API_KEY) {
try {
const data = await avHistory(symbol);
historyCache.set(cacheKey, { data, ts: now });
return { data, error: null, stale: false };
} catch (e) {
lastError = String(e);
}
}
if (cached) {
return { data: cached.data, error: lastError, stale: true };
}
return { data: null, errortoMarketSymbol function · typescript · L358-L363 (6 LOC)src/services/marketData.ts
export function toMarketSymbol(symbol: string, assetType: string): string {
if (assetType === "CRYPTO" && !symbol.includes("-")) {
return `${symbol.toUpperCase()}-USD`;
}
return symbol.toUpperCase();
}getFXRate function · typescript · L370-L375 (6 LOC)src/services/marketData.ts
export async function getFXRate(targetCurrency: string): Promise<number> {
if (targetCurrency === "USD") return 1;
const result = await getQuote(`${targetCurrency}USD=X`);
if (!result.data || result.data.price <= 0 || !isFinite(result.data.price)) return 1;
return 1 / result.data.price;
}getPortfolios function · typescript · L5-L11 (7 LOC)src/services/portfolio.service.ts
export async function getPortfolios(userId: string) {
return prisma.portfolio.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
include: { _count: { select: { assets: true } } },
});
}getPortfolioById function · typescript · L13-L20 (8 LOC)src/services/portfolio.service.ts
export async function getPortfolioById(id: string) {
return prisma.portfolio.findUnique({
where: { id },
include: {
assets: { include: { transactions: true } },
},
});
}Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
getPortfolioSummary function · typescript · L32-L72 (41 LOC)src/services/portfolio.service.ts
export async function getPortfolioSummary(
id: string
): Promise<PortfolioSummary | null> {
const portfolio = await prisma.portfolio.findUnique({
where: { id },
include: { assets: true },
});
if (!portfolio) return null;
const totalCost = portfolio.assets.reduce(
(sum, a) => sum + a.quantity * a.averageBuyPrice,
0
);
// Fetch live prices for all assets
const marketSymbols = portfolio.assets.map((a) =>
toMarketSymbol(a.symbol, a.assetType)
);
const quotes = await getBatchQuotes(marketSymbols);
const totalValue = portfolio.assets.reduce((sum, a) => {
const mSym = toMarketSymbol(a.symbol, a.assetType);
const quote = quotes.get(mSym);
const price = quote?.price ?? a.averageBuyPrice;
return sum + a.quantity * price;
}, 0);
const totalGainLoss = totalValue - totalCost;
const totalGainLossPct = totalCost > 0 ? totalGainLoss / totalCost : 0;
return {
id: portfolio.id,
name: portfolio.name,
totalValue,
totalgetAssetById function · typescript · L74-L79 (6 LOC)src/services/portfolio.service.ts
export async function getAssetById(id: string, userId: string) {
return prisma.asset.findFirst({
where: { id, portfolio: { userId } },
include: { portfolio: { select: { id: true, name: true } } },
});
}getAssetsByUser function · typescript · L81-L87 (7 LOC)src/services/portfolio.service.ts
export async function getAssetsByUser(userId: string) {
return prisma.asset.findMany({
where: { portfolio: { userId } },
include: { portfolio: { select: { id: true, name: true } } },
orderBy: [{ portfolio: { name: "asc" } }, { symbol: "asc" }],
});
}updateAsset function · typescript · L99-L110 (12 LOC)src/services/portfolio.service.ts
export async function updateAsset(id: string, data: UpdateAssetInput) {
const { assetType, ...rest } = data;
return prisma.asset.update({
where: { id },
data: {
...rest,
...(assetType && {
assetType,
}),
},
});
}getAssetBySymbol function · typescript · L116-L121 (6 LOC)src/services/portfolio.service.ts
export async function getAssetBySymbol(symbol: string, userId: string) {
return prisma.asset.findFirst({
where: { symbol, portfolio: { userId } },
include: { portfolio: { select: { id: true, name: true } } },
});
}createAsset function · typescript · L123-L136 (14 LOC)src/services/portfolio.service.ts
export async function createAsset(input: CreateAssetInput) {
return prisma.asset.create({
data: {
portfolioId: input.portfolioId,
symbol: input.symbol,
name: input.name,
assetType: input.assetType,
quantity: input.quantity,
averageBuyPrice: input.averageBuyPrice,
currency: input.currency,
notes: input.notes,
},
});
}getTransactionsByUser function · typescript · L5-L15 (11 LOC)src/services/transaction.service.ts
export async function getTransactionsByUser(userId: string) {
return prisma.transaction.findMany({
where: { asset: { portfolio: { userId } } },
include: {
asset: {
include: { portfolio: { select: { id: true, name: true } } },
},
},
orderBy: { date: "desc" },
});
}getTransactions function · typescript · L17-L22 (6 LOC)src/services/transaction.service.ts
export async function getTransactions(assetId: string) {
return prisma.transaction.findMany({
where: { assetId },
orderBy: { date: "desc" },
});
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
createTransaction function · typescript · L24-L40 (17 LOC)src/services/transaction.service.ts
export async function createTransaction(input: CreateTransactionInput) {
const transaction = await prisma.transaction.create({
data: {
assetId: input.assetId,
type: input.type as TransactionType,
quantity: input.quantity,
pricePerUnit: input.pricePerUnit,
fees: input.fees ?? 0,
date: new Date(input.date),
notes: input.notes,
},
});
await syncAssetAfterTransaction(input.assetId, input);
return transaction;
}syncAssetAfterTransaction function · typescript · L42-L82 (41 LOC)src/services/transaction.service.ts
async function syncAssetAfterTransaction(
assetId: string,
input: CreateTransactionInput
) {
if (input.type === "DIVIDEND") return;
const asset = await prisma.asset.findUnique({ where: { id: assetId } });
if (!asset) {
throw new Error(`Asset not found: ${assetId}`);
}
if (input.type === "BUY") {
if (input.quantity <= 0) {
throw new Error("Buy quantity must be positive.");
}
const newQuantity = asset.quantity + input.quantity;
const newAverage =
newQuantity > 0
? (asset.quantity * asset.averageBuyPrice + input.quantity * input.pricePerUnit) /
newQuantity
: 0;
await prisma.asset.update({
where: { id: assetId },
data: { quantity: newQuantity, averageBuyPrice: newAverage },
});
} else if (input.type === "SELL") {
if (input.quantity <= 0) {
throw new Error("Sell quantity must be positive.");
}
if (asset.quantity < input.quantity) {
throw new Error(
`Insufficient qgetUserProfile function · typescript · L8-L15 (8 LOC)src/services/user.service.ts
export async function getUserProfile(id: string): Promise<UserProfile | null> {
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, defaultCurrency: true, createdAt: true, password: true },
});
if (!user) return null;
return { ...user, createdAt: user.createdAt.toISOString(), hasPassword: !!user.password };
}updateUserProfile function · typescript · L17-L22 (6 LOC)src/services/user.service.ts
export async function updateUserProfile(
id: string,
data: { name?: string; email?: string; defaultCurrency?: string }
) {
return prisma.user.update({ where: { id }, data });
}getUserExportData function · typescript · L32-L74 (43 LOC)src/services/user.service.ts
export async function getUserExportData(id: string) {
return prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
defaultCurrency: true,
createdAt: true,
portfolios: {
select: {
id: true,
name: true,
createdAt: true,
assets: {
select: {
id: true,
symbol: true,
name: true,
assetType: true,
quantity: true,
averageBuyPrice: true,
currency: true,
notes: true,
createdAt: true,
transactions: {
select: {
id: true,
type: true,
quantity: true,
pricePerUnit: true,
fees: true,
date: true,
notes: true,
},
},
},
},
},
},
getUserAssetsFlat function · typescript · L76-L99 (24 LOC)src/services/user.service.ts
export async function getUserAssetsFlat(id: string) {
const portfolios = await prisma.portfolio.findMany({
where: { userId: id },
select: {
name: true,
assets: {
select: {
id: true,
symbol: true,
name: true,
assetType: true,
quantity: true,
averageBuyPrice: true,
currency: true,
notes: true,
createdAt: true,
},
},
},
});
return portfolios.flatMap((p) =>
p.assets.map((a) => ({ ...a, portfolioName: p.name }))
);
}getUserTransactionsFlat function · typescript · L101-L122 (22 LOC)src/services/user.service.ts
export async function getUserTransactionsFlat(id: string) {
return prisma.transaction.findMany({
where: { asset: { portfolio: { userId: id } } },
select: {
id: true,
type: true,
quantity: true,
pricePerUnit: true,
fees: true,
date: true,
notes: true,
asset: {
select: {
symbol: true,
name: true,
portfolio: { select: { name: true } },
},
},
},
orderBy: { date: "desc" },
});
}getWatchlistByUser function · typescript · L4-L9 (6 LOC)src/services/watchlist.service.ts
export async function getWatchlistByUser(userId: string) {
return prisma.watchlistItem.findMany({
where: { userId },
orderBy: { addedAt: "desc" },
});
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
addToWatchlist function · typescript · L11-L24 (14 LOC)src/services/watchlist.service.ts
export async function addToWatchlist(
userId: string,
input: CreateWatchlistItemInput
) {
return prisma.watchlistItem.create({
data: {
userId,
symbol: input.symbol.trim().toUpperCase(),
name: input.name.trim(),
assetType: input.assetType,
notes: input.notes?.trim() || null,
},
});
}formatCurrency function · typescript · L5-L16 (12 LOC)src/utils/format.ts
export function formatCurrency(
value: number,
currency = "USD",
locale = "en-US"
): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}formatCompact function · typescript · L31-L36 (6 LOC)src/utils/format.ts
export function formatCompact(value: number, locale = "en-US"): string {
return new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: 1,
}).format(value);
}