SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
11 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.1 KB · 56 lines typescript
Raw Blame History
1import { eq, sql } from 'drizzle-orm';2import type { Database } from '@rareindex/database';3import { collections, collectionSnapshots } from '@rareindex/database';4import { logger } from '@rareindex/shared';56const log = logger.child({ job: 'account.snapshots' });78/**9 * Daily portfolio snapshot per collection: Σ value (variant RIV → asset RIV → manual value) and10 * Σ cost basis in USD. Idempotent for a given date (upsert).11 */12export async function runPortfolioSnapshots(db: Database, date = new Date().toISOString().slice(0, 10)): Promise<number> {13  const rows = (await db.execute(sql`14    select c.id as collection_id,15           coalesce(sum(coalesce(vs.riv_usd, s.riv_usd, ci.manual_value_usd, 0) * greatest(ci.quantity, 1)), 0)::float as value_usd,16           coalesce(sum(coalesce(ci.purchase_price_usd, 0) * greatest(ci.quantity, 1)), 0)::float as cost_basis_usd,17           count(ci.id)::int as items18    from collections c19    left join collection_items ci on ci.collection_id = c.id and ci.sold_at is null20    left join asset_stats s on s.asset_id = ci.asset_id21    left join variant_stats vs on vs.variant_id = ci.variant_id22    group by c.id23  `)) as unknown as Array<{ collection_id: string; value_usd: number; cost_basis_usd: number; items: number }>;24  let n = 0;25  for (const r of rows) {26    if (r.items === 0) continue;27    await db28      .insert(collectionSnapshots)29      .values({ collectionId: r.collection_id, date, valueUsd: Number(r.value_usd), costBasisUsd: Number(r.cost_basis_usd), items: r.items })30      .onConflictDoUpdate({ target: [collectionSnapshots.collectionId, collectionSnapshots.date], set: { valueUsd: Number(r.value_usd), costBasisUsd: Number(r.cost_basis_usd), items: r.items } });31    n++;32  }33  log.info({ collections: n, date }, 'portfolio snapshots written');34  return n;35}3637export async function snapshotOne(db: Database, collectionId: string): Promise<void> {38  const date = new Date().toISOString().slice(0, 10);39  const rows = (await db.execute(sql`40    select coalesce(sum(coalesce(vs.riv_usd, s.riv_usd, ci.manual_value_usd, 0) * greatest(ci.quantity, 1)), 0)::float as value_usd,41           coalesce(sum(coalesce(ci.purchase_price_usd, 0) * greatest(ci.quantity, 1)), 0)::float as cost_basis_usd,42           count(ci.id)::int as items43    from collection_items ci44    left join asset_stats s on s.asset_id = ci.asset_id45    left join variant_stats vs on vs.variant_id = ci.variant_id46    where ci.collection_id = ${collectionId} and ci.sold_at is null47  `)) as unknown as Array<{ value_usd: number; cost_basis_usd: number; items: number }>;48  const r = rows[0];49  if (!r || r.items === 0) return;50  await db51    .insert(collectionSnapshots)52    .values({ collectionId, date, valueUsd: Number(r.value_usd), costBasisUsd: Number(r.cost_basis_usd), items: r.items })53    .onConflictDoUpdate({ target: [collectionSnapshots.collectionId, collectionSnapshots.date], set: { valueUsd: Number(r.value_usd), costBasisUsd: Number(r.cost_basis_usd), items: r.items } });54  await db.update(collections).set({ updatedAt: new Date() }).where(eq(collections.id, collectionId));55}56