From c1bcac8a3079258a42bf4665ca350b3c5fb872c3 Mon Sep 17 00:00:00 2001 From: Mike Johnston Date: Fri, 20 Mar 2026 14:37:17 -0400 Subject: [PATCH] fix: implement fund dissolution for insolvent funds after SELL_SHORT trades --- src/app/api/admin/funds/[fundId]/route.ts | 2 +- src/app/api/trade/route.ts | 65 ++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/app/api/admin/funds/[fundId]/route.ts b/src/app/api/admin/funds/[fundId]/route.ts index a5b6221..27046a6 100644 --- a/src/app/api/admin/funds/[fundId]/route.ts +++ b/src/app/api/admin/funds/[fundId]/route.ts @@ -112,7 +112,7 @@ export async function DELETE( const portfolioValue = fund.user.positions.reduce((sum, p) => { const val = p.positionType === 'LONG' ? p.shares * p.hashtag.currentPrice - : p.avgBuyPrice * p.shares - (p.hashtag.currentPrice - p.avgBuyPrice) * p.shares + : (2 * p.avgBuyPrice - p.hashtag.currentPrice) * p.shares return sum + val }, 0) const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding) diff --git a/src/app/api/trade/route.ts b/src/app/api/trade/route.ts index 7ef0cf9..338b0dc 100644 --- a/src/app/api/trade/route.ts +++ b/src/app/api/trade/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' -import { calcTrade } from '@/lib/pricing' +import { calcTrade, calcFundNav } from '@/lib/pricing' import { formatCurrency } from '@/lib/utils' import { z } from 'zod' @@ -169,5 +169,68 @@ export async function POST(req: NextRequest) { } } + // If this was a fund's SELL_SHORT and the fund is now insolvent, dissolve it + if (type === 'SELL_SHORT' && fundId) { + const updatedFundUser = await prisma.user.findUnique({ + where: { id: user.id }, + select: { balance: true, fund: { select: { id: true } } }, + }) + if (updatedFundUser && updatedFundUser.balance < 0 && updatedFundUser.fund) { + await dissolveFund(updatedFundUser.fund.id) + return NextResponse.json({ ok: true, dissolved: true }) + } + } + return NextResponse.json({ ok: true }) } + +/** + * Dissolves an insolvent fund: pays investors at current mark-to-market NAV + * (floored to $0), then deletes the HedgeFund record and its shadow User. + * Called automatically when a SELL_SHORT trade pushes a fund balance negative. + */ +async function dissolveFund(fundId: string) { + const fund = await prisma.hedgeFund.findUnique({ + where: { id: fundId }, + select: { + id: true, + userId: true, + sharesOutstanding: true, + investments: { select: { userId: true, shares: true } }, + user: { + select: { + balance: true, + positions: { + where: { shares: { gt: 0 } }, + select: { + shares: true, + avgBuyPrice: true, + positionType: true, + hashtag: { select: { currentPrice: true } }, + }, + }, + }, + }, + }, + }) + if (!fund) return + + const portfolioValue = fund.user.positions.reduce((sum, p) => { + const val = + p.positionType === 'LONG' + ? p.shares * p.hashtag.currentPrice + : (2 * p.avgBuyPrice - p.hashtag.currentPrice) * p.shares + return sum + val + }, 0) + const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding) + + for (const inv of fund.investments) { + const payout = Math.max(0, inv.shares * nav) + if (payout > 0) { + await prisma.user.update({ where: { id: inv.userId }, data: { balance: { increment: payout } } }) + } + } + + await prisma.hedgeFund.delete({ where: { id: fund.id } }) + await prisma.user.delete({ where: { id: fund.userId } }) +}