added udner construction for file compaction, planning for unbloating
All checks were successful
Deploy / deploy (push) Successful in 1m28s
Security Tests / security-non-db (push) Successful in 20s
Security Tests / security-db (push) Successful in 25s

This commit is contained in:
2026-03-15 14:44:47 -05:00
parent 512e21276c
commit ba549f6c84
14 changed files with 663 additions and 31 deletions

View File

@@ -0,0 +1,64 @@
export class ApiError extends Error {
statusCode: number;
code: string;
details?: unknown;
constructor(statusCode: number, code: string, message: string, details?: unknown) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
export function badRequest(code: string, message: string, details?: unknown) {
return new ApiError(400, code, message, details);
}
export function unauthorized(code: string, message: string, details?: unknown) {
return new ApiError(401, code, message, details);
}
export function forbidden(code: string, message: string, details?: unknown) {
return new ApiError(403, code, message, details);
}
export function notFound(code: string, message: string, details?: unknown) {
return new ApiError(404, code, message, details);
}
export function conflict(code: string, message: string, details?: unknown) {
return new ApiError(409, code, message, details);
}
export function toErrorBody(err: unknown, requestId: string) {
if (err instanceof ApiError) {
return {
statusCode: err.statusCode,
body: {
ok: false,
code: err.code,
message: err.message,
requestId,
...(err.details !== undefined ? { details: err.details } : {}),
},
};
}
const fallback = err as { statusCode?: number; code?: string; message?: string };
const statusCode =
typeof fallback?.statusCode === "number" ? fallback.statusCode : 500;
return {
statusCode,
body: {
ok: false,
code: fallback?.code ?? "INTERNAL",
message:
statusCode >= 500
? "Something went wrong"
: fallback?.message ?? "Bad request",
requestId,
},
};
}

View File

@@ -0,0 +1,70 @@
import type { Prisma, PrismaClient } from "@prisma/client";
type BudgetSessionAccessor =
| Pick<PrismaClient, "budgetSession">
| Pick<Prisma.TransactionClient, "budgetSession">;
function normalizeAvailableCents(value: number): bigint {
if (!Number.isFinite(value)) return 0n;
return BigInt(Math.max(0, Math.trunc(value)));
}
export async function getLatestBudgetSession(
db: BudgetSessionAccessor,
userId: string
) {
return db.budgetSession.findFirst({
where: { userId },
orderBy: { periodStart: "desc" },
});
}
export async function ensureBudgetSession(
db: BudgetSessionAccessor,
userId: string,
fallbackAvailableCents = 0,
now = new Date()
) {
const existing = await getLatestBudgetSession(db, userId);
if (existing) return existing;
const start = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0)
);
const end = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0)
);
const normalizedAvailable = normalizeAvailableCents(fallbackAvailableCents);
return db.budgetSession.create({
data: {
userId,
periodStart: start,
periodEnd: end,
totalBudgetCents: normalizedAvailable,
allocatedCents: 0n,
fundedCents: 0n,
availableCents: normalizedAvailable,
},
});
}
export async function ensureBudgetSessionAvailableSynced(
db: BudgetSessionAccessor,
userId: string,
availableCents: number
) {
const normalizedAvailable = normalizeAvailableCents(availableCents);
const session = await ensureBudgetSession(
db,
userId,
Number(normalizedAvailable)
);
if ((session.availableCents ?? 0n) === normalizedAvailable) return session;
return db.budgetSession.update({
where: { id: session.id },
data: { availableCents: normalizedAvailable },
});
}

View File

@@ -0,0 +1,200 @@
export type PercentCategory = {
id: string;
percent: number;
balanceCents: bigint | number | null;
};
type ShareRow = {
id: string;
share: number;
};
type ShareResult =
| { ok: true; shares: ShareRow[] }
| { ok: false; reason: "no_percent" | "insufficient_balances" };
function toWholeCents(value: bigint | number | null | undefined): number {
if (typeof value === "bigint") return Number(value);
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
return 0;
}
function normalizedAmountCents(amountCents: number): number {
if (!Number.isFinite(amountCents)) return 0;
return Math.max(0, Math.trunc(amountCents));
}
export function computePercentShares(
categories: PercentCategory[],
amountCents: number
): ShareResult {
const percentTotal = categories.reduce((sum, cat) => sum + cat.percent, 0);
if (percentTotal <= 0) return { ok: false, reason: "no_percent" };
const shares = categories.map((cat) => {
const raw = (normalizedAmountCents(amountCents) * cat.percent) / percentTotal;
const floored = Math.floor(raw);
return {
id: cat.id,
balanceCents: toWholeCents(cat.balanceCents),
share: floored,
frac: raw - floored,
};
});
let remainder =
normalizedAmountCents(amountCents) - shares.reduce((sum, s) => sum + s.share, 0);
shares
.slice()
.sort((a, b) => b.frac - a.frac)
.forEach((s) => {
if (remainder > 0) {
s.share += 1;
remainder -= 1;
}
});
if (shares.some((s) => s.share > s.balanceCents)) {
return { ok: false, reason: "insufficient_balances" };
}
return { ok: true, shares: shares.map(({ id, share }) => ({ id, share })) };
}
export function computeWithdrawShares(
categories: PercentCategory[],
amountCents: number
): ShareResult {
const percentTotal = categories.reduce((sum, cat) => sum + cat.percent, 0);
if (percentTotal <= 0) return { ok: false, reason: "no_percent" };
const working = categories.map((cat) => ({
id: cat.id,
percent: cat.percent,
balanceCents: toWholeCents(cat.balanceCents),
share: 0,
}));
let remaining = normalizedAmountCents(amountCents);
let safety = 0;
while (remaining > 0 && safety < 1000) {
safety += 1;
const eligible = working.filter((c) => c.balanceCents > 0 && c.percent > 0);
if (eligible.length === 0) break;
const totalPercent = eligible.reduce((sum, cat) => sum + cat.percent, 0);
if (totalPercent <= 0) break;
const provisional = eligible.map((cat) => {
const raw = (remaining * cat.percent) / totalPercent;
const floored = Math.floor(raw);
return {
id: cat.id,
raw,
floored,
remainder: raw - floored,
};
});
let sumBase = provisional.reduce((sum, p) => sum + p.floored, 0);
let leftovers = remaining - sumBase;
provisional
.slice()
.sort((a, b) => b.remainder - a.remainder)
.forEach((p) => {
if (leftovers > 0) {
p.floored += 1;
leftovers -= 1;
}
});
let allocatedThisRound = 0;
for (const p of provisional) {
const entry = working.find((w) => w.id === p.id);
if (!entry) continue;
const take = Math.min(p.floored, entry.balanceCents);
if (take > 0) {
entry.balanceCents -= take;
entry.share += take;
allocatedThisRound += take;
}
}
remaining -= allocatedThisRound;
if (allocatedThisRound === 0) break;
}
if (remaining > 0) {
return { ok: false, reason: "insufficient_balances" };
}
return {
ok: true,
shares: working.map((c) => ({ id: c.id, share: c.share })),
};
}
export function computeOverdraftShares(
categories: PercentCategory[],
amountCents: number
): ShareResult {
const percentTotal = categories.reduce((sum, cat) => sum + cat.percent, 0);
if (percentTotal <= 0) return { ok: false, reason: "no_percent" };
const shares = categories.map((cat) => {
const raw = (normalizedAmountCents(amountCents) * cat.percent) / percentTotal;
const floored = Math.floor(raw);
return {
id: cat.id,
share: floored,
frac: raw - floored,
};
});
let remainder =
normalizedAmountCents(amountCents) - shares.reduce((sum, s) => sum + s.share, 0);
shares
.slice()
.sort((a, b) => b.frac - a.frac)
.forEach((s) => {
if (remainder > 0) {
s.share += 1;
remainder -= 1;
}
});
return { ok: true, shares: shares.map(({ id, share }) => ({ id, share })) };
}
export function computeDepositShares(
categories: PercentCategory[],
amountCents: number
): ShareResult {
const percentTotal = categories.reduce((sum, cat) => sum + cat.percent, 0);
if (percentTotal <= 0) return { ok: false, reason: "no_percent" };
const shares = categories.map((cat) => {
const raw = (normalizedAmountCents(amountCents) * cat.percent) / percentTotal;
const floored = Math.floor(raw);
return {
id: cat.id,
share: floored,
frac: raw - floored,
};
});
let remainder =
normalizedAmountCents(amountCents) - shares.reduce((sum, s) => sum + s.share, 0);
shares
.slice()
.sort((a, b) => b.frac - a.frac)
.forEach((s) => {
if (remainder > 0) {
s.share += 1;
remainder -= 1;
}
});
return { ok: true, shares: shares.map(({ id, share }) => ({ id, share })) };
}

View File

@@ -0,0 +1,19 @@
import type { Prisma, PrismaClient } from "@prisma/client";
export const DEFAULT_USER_TIMEZONE = "America/New_York";
type UserReader =
| Pick<PrismaClient, "user">
| Pick<Prisma.TransactionClient, "user">;
export async function getUserTimezone(
db: UserReader,
userId: string,
fallback: string = DEFAULT_USER_TIMEZONE
): Promise<string> {
const user = await db.user.findUnique({
where: { id: userId },
select: { timezone: true },
});
return user?.timezone ?? fallback;
}