fix: implement rounding for monetary values across fund and user transactions
Build Images and Deploy / Update-PROD-Stack (push) Failing after 33s
Build Images and Deploy / Update-PROD-Stack (push) Failing after 33s
This commit is contained in:
@@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth'
|
|||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
const patchSchema = z.object({
|
const patchSchema = z.object({
|
||||||
addManagerUsername: z.string().optional(),
|
addManagerUsername: z.string().optional(),
|
||||||
@@ -58,7 +58,7 @@ export async function PATCH(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof balance === 'number') {
|
if (typeof balance === 'number') {
|
||||||
await prisma.user.update({ where: { id: fund.userId }, data: { balance } })
|
await prisma.user.update({ where: { id: fund.userId }, data: { balance: round2(balance) } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await prisma.hedgeFund.findUnique({
|
const updated = await prisma.hedgeFund.findUnique({
|
||||||
@@ -112,14 +112,14 @@ export async function DELETE(
|
|||||||
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
||||||
const val = p.positionType === 'LONG'
|
const val = p.positionType === 'LONG'
|
||||||
? p.shares * p.hashtag.currentPrice
|
? 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
|
return sum + val
|
||||||
}, 0)
|
}, 0)
|
||||||
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
||||||
|
|
||||||
// Pay out each investor at current NAV before wiping records
|
// Pay out each investor at current NAV before wiping records
|
||||||
for (const inv of fund.investments) {
|
for (const inv of fund.investments) {
|
||||||
const payout = Math.max(0, inv.shares * nav)
|
const payout = Math.max(0, round2(inv.shares * nav))
|
||||||
if (payout > 0) {
|
if (payout > 0) {
|
||||||
await prisma.user.update({ where: { id: inv.userId }, data: { balance: { increment: payout } } })
|
await prisma.user.update({ where: { id: inv.userId }, data: { balance: { increment: payout } } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
const STARTING_BALANCE = 2000
|
const STARTING_BALANCE = 2000
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ export async function POST(
|
|||||||
}, 0)
|
}, 0)
|
||||||
const fundTotalValue = inv.fund.user.balance + fundPortfolioValue
|
const fundTotalValue = inv.fund.user.balance + fundPortfolioValue
|
||||||
const nav = calcFundNav(fundTotalValue, inv.fund.sharesOutstanding)
|
const nav = calcFundNav(fundTotalValue, inv.fund.sharesOutstanding)
|
||||||
const payout = Math.max(0, inv.shares * nav)
|
const payout = Math.max(0, round2(inv.shares * nav))
|
||||||
return [
|
return [
|
||||||
prisma.hedgeFund.update({
|
prisma.hedgeFund.update({
|
||||||
where: { id: inv.fundId },
|
where: { id: inv.fundId },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth'
|
|||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
balance: z.number().min(0).optional(),
|
balance: z.number().min(0).optional(),
|
||||||
@@ -92,13 +92,13 @@ export async function DELETE(
|
|||||||
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
||||||
const val = p.positionType === 'LONG'
|
const val = p.positionType === 'LONG'
|
||||||
? p.shares * p.hashtag.currentPrice
|
? 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
|
return sum + val
|
||||||
}, 0)
|
}, 0)
|
||||||
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
||||||
const payout = inv.shares * nav
|
const payout = round2(Math.max(0, inv.shares * nav))
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.user.update({ where: { id: fund.userId }, data: { balance: { decrement: payout } } }),
|
prisma.user.update({ where: { id: fund.userId }, data: { balance: { decrement: round2(Math.max(0, payout)) } } }),
|
||||||
prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { decrement: inv.shares } } }),
|
prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { decrement: inv.shares } } }),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
export async function POST(req: NextRequest, { params }: { params: { slug: string } }) {
|
export async function POST(req: NextRequest, { params }: { params: { slug: string } }) {
|
||||||
const session = await getServerSession(authOptions)
|
const session = await getServerSession(authOptions)
|
||||||
@@ -10,7 +10,7 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin
|
|||||||
|
|
||||||
const slug = decodeURIComponent(params.slug).toLowerCase()
|
const slug = decodeURIComponent(params.slug).toLowerCase()
|
||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const amount = Number(body.amount)
|
const amount = round2(Number(body.amount))
|
||||||
|
|
||||||
if (!amount || amount < 1) {
|
if (!amount || amount < 1) {
|
||||||
return NextResponse.json({ error: 'Minimum investment is $1' }, { status: 400 })
|
return NextResponse.json({ error: 'Minimum investment is $1' }, { status: 400 })
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
export async function POST(req: NextRequest, { params }: { params: { slug: string } }) {
|
export async function POST(req: NextRequest, { params }: { params: { slug: string } }) {
|
||||||
const session = await getServerSession(authOptions)
|
const session = await getServerSession(authOptions)
|
||||||
@@ -50,7 +50,7 @@ export async function POST(req: NextRequest, { params }: { params: { slug: strin
|
|||||||
}, 0)
|
}, 0)
|
||||||
const totalValue = fund.user.balance + portfolioValue
|
const totalValue = fund.user.balance + portfolioValue
|
||||||
const nav = calcFundNav(totalValue, fund.sharesOutstanding)
|
const nav = calcFundNav(totalValue, fund.sharesOutstanding)
|
||||||
const payout = sharesToRedeem * nav
|
const payout = round2(sharesToRedeem * nav)
|
||||||
|
|
||||||
if (fund.user.balance < payout) {
|
if (fund.user.balance < payout) {
|
||||||
return NextResponse.json({ error: 'Fund has insufficient cash to redeem. Try a smaller amount.' }, { status: 400 })
|
return NextResponse.json({ error: 'Fund has insufficient cash to redeem. Try a smaller amount.' }, { status: 400 })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { round2 } from '@/lib/pricing'
|
||||||
import { GRID_SIZE, PRIZE_MAP } from '@/lib/lottery'
|
import { GRID_SIZE, PRIZE_MAP } from '@/lib/lottery'
|
||||||
|
|
||||||
function buildPrizes(): number[] {
|
function buildPrizes(): number[] {
|
||||||
@@ -68,7 +69,7 @@ export async function POST(req: NextRequest) {
|
|||||||
prisma.user.update({
|
prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: {
|
data: {
|
||||||
balance: { increment: winAmount },
|
balance: { increment: round2(winAmount) },
|
||||||
lastLotteryAt: now,
|
lastLotteryAt: now,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcTrade } from '@/lib/pricing'
|
import { calcTrade, round2 } from '@/lib/pricing'
|
||||||
import { formatCurrency } from '@/lib/utils'
|
import { formatCurrency } from '@/lib/utils'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export async function POST(req: NextRequest) {
|
|||||||
// Update user balance
|
// Update user balance
|
||||||
await tx.user.update({
|
await tx.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { balance: { increment: balanceDelta } },
|
data: { balance: { increment: round2(balanceDelta) } },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update / create position
|
// Update / create position
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
const STARTING_BALANCE = 2000
|
const STARTING_BALANCE = 2000
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
const fundTotalValue = inv.fund.user.balance + fundPortfolioValue
|
const fundTotalValue = inv.fund.user.balance + fundPortfolioValue
|
||||||
const nav = calcFundNav(fundTotalValue, inv.fund.sharesOutstanding)
|
const nav = calcFundNav(fundTotalValue, inv.fund.sharesOutstanding)
|
||||||
const payout = Math.max(0, inv.shares * nav)
|
const payout = Math.max(0, round2(inv.shares * nav))
|
||||||
return [
|
return [
|
||||||
prisma.hedgeFund.update({
|
prisma.hedgeFund.update({
|
||||||
where: { id: inv.fundId },
|
where: { id: inv.fundId },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|||||||
import { getServerSession } from 'next-auth'
|
import { getServerSession } from 'next-auth'
|
||||||
import { authOptions } from '@/lib/auth'
|
import { authOptions } from '@/lib/auth'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { calcFundNav } from '@/lib/pricing'
|
import { calcFundNav, round2 } from '@/lib/pricing'
|
||||||
|
|
||||||
const USERNAME_RE = /^[a-z0-9_]{3,20}$/ // validated after toLowerCase
|
const USERNAME_RE = /^[a-z0-9_]{3,20}$/ // validated after toLowerCase
|
||||||
|
|
||||||
@@ -135,11 +135,11 @@ export async function DELETE() {
|
|||||||
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
const portfolioValue = fund.user.positions.reduce((sum, p) => {
|
||||||
const val = p.positionType === 'LONG'
|
const val = p.positionType === 'LONG'
|
||||||
? p.shares * p.hashtag.currentPrice
|
? 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
|
return sum + val
|
||||||
}, 0)
|
}, 0)
|
||||||
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
const nav = calcFundNav(fund.user.balance + portfolioValue, fund.sharesOutstanding)
|
||||||
const payout = inv.shares * nav
|
const payout = round2(Math.max(0, inv.shares * nav))
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.user.update({ where: { id: fund.userId }, data: { balance: { decrement: payout } } }),
|
prisma.user.update({ where: { id: fund.userId }, data: { balance: { decrement: payout } } }),
|
||||||
prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { decrement: inv.shares } } }),
|
prisma.hedgeFund.update({ where: { id: fund.id }, data: { sharesOutstanding: { decrement: inv.shares } } }),
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export function getBalanceTier(balance: number): BalanceTier {
|
|||||||
return { level: 1, pointsPerDay: 1, nextThreshold: 10_000 }
|
return { level: 1, pointsPerDay: 1, nextThreshold: 10_000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Round a dollar amount to 2 decimal places for DB storage. */
|
||||||
|
export const round2 = (n: number) => Math.round(n * 100) / 100
|
||||||
|
|
||||||
/** Calculate NAV (net asset value) per fund share. Returns 1.00 if no shares outstanding. */
|
/** Calculate NAV (net asset value) per fund share. Returns 1.00 if no shares outstanding. */
|
||||||
export function calcFundNav(totalValue: number, sharesOutstanding: number): number {
|
export function calcFundNav(totalValue: number, sharesOutstanding: number): number {
|
||||||
if (sharesOutstanding <= 0) return 1.00
|
if (sharesOutstanding <= 0) return 1.00
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ async function forceClosePositions(hashtagId: string, price: number, tag: string
|
|||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.user.update({
|
prisma.user.update({
|
||||||
where: { id: pos.userId },
|
where: { id: pos.userId },
|
||||||
data: { balance: { increment: balanceDelta } },
|
data: { balance: { increment: round2(balanceDelta) } },
|
||||||
}),
|
}),
|
||||||
prisma.position.update({
|
prisma.position.update({
|
||||||
where: { id: pos.id },
|
where: { id: pos.id },
|
||||||
|
|||||||
Reference in New Issue
Block a user