← back to idaturgutinal__portfolio-tracker

Function bodies 201 total

All specs Real LLM only Function bodies
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 this
sendPasswordResetEmail 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 style
required 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.targetP
getDashboardData 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 = [];
  for
getDividendData 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: isFi
avHistory 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)}&quotesCount=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, error
toMarketSymbol 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,
    total
getAssetById 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 q
getUserProfile 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);
}
‹ prevpage 4 / 5next ›