added api logic, vitest, minimal testing ui

This commit is contained in:
2025-11-15 23:26:57 -06:00
parent f4160b91db
commit 4eae966f96
95 changed files with 14155 additions and 469 deletions

View File

@@ -0,0 +1,85 @@
import { FastifyPluginAsync } from "fastify";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import { prisma } from "../prisma.js";
const NewCat = z.object({
name: z.string().min(1).max(100),
percent: z.number().int().min(0).max(100),
isSavings: z.boolean().default(false),
priority: z.number().int().min(0).max(10_000),
});
const PatchCat = NewCat.partial();
const IdParam = z.object({ id: z.string().min(1) });
async function assertPercentTotal100(tx: Prisma.TransactionClient, userId: string) {
const g = await tx.variableCategory.groupBy({
by: ["userId"],
where: { userId },
_sum: { percent: true },
});
const sum = g[0]?._sum.percent ?? 0;
if (sum !== 100) {
const err: any = new Error(`Percents must sum to 100 (got ${sum}).`);
err.statusCode = 400;
err.code = "PERCENT_TOTAL_NOT_100";
throw err;
}
}
const plugin: FastifyPluginAsync = async (app) => {
// CREATE
app.post("/api/variable-categories", async (req, reply) => {
const userId = req.userId;
const body = NewCat.safeParse(req.body);
if (!body.success) return reply.status(400).send({ error: "INVALID_BODY", details: body.error.flatten() });
const created = await prisma.$transaction(async (tx) => {
const rec = await tx.variableCategory.create({
data: { ...body.data, userId },
select: { id: true },
});
await assertPercentTotal100(tx, userId);
return rec;
});
return reply.status(201).send(created);
});
// UPDATE
app.patch("/api/variable-categories/:id", async (req, reply) => {
const userId = req.userId;
const pid = IdParam.safeParse(req.params);
const patch = PatchCat.safeParse(req.body);
if (!pid.success) return reply.status(400).send({ error: "INVALID_ID", details: pid.error.flatten() });
if (!patch.success) return reply.status(400).send({ error: "INVALID_BODY", details: patch.error.flatten() });
await prisma.$transaction(async (tx) => {
const exists = await tx.variableCategory.findFirst({ where: { id: pid.data.id, userId } });
if (!exists) return reply.status(404).send({ error: "NOT_FOUND" });
await tx.variableCategory.update({ where: { id: pid.data.id }, data: patch.data });
await assertPercentTotal100(tx, userId);
});
return reply.send({ ok: true });
});
// DELETE
app.delete("/api/variable-categories/:id", async (req, reply) => {
const userId = req.userId;
const pid = IdParam.safeParse(req.params);
if (!pid.success) return reply.status(400).send({ error: "INVALID_ID", details: pid.error.flatten() });
await prisma.$transaction(async (tx) => {
const exists = await tx.variableCategory.findFirst({ where: { id: pid.data.id, userId } });
if (!exists) return reply.status(404).send({ error: "NOT_FOUND" });
await tx.variableCategory.delete({ where: { id: pid.data.id } });
await assertPercentTotal100(tx, userId);
});
return reply.send({ ok: true });
});
};
export default plugin;