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,94 @@
import { FastifyPluginAsync } from "fastify";
import { z } from "zod";
import { prisma } from "../prisma.js";
const NewPlan = z.object({
name: z.string().min(1).max(120),
totalCents: z.number().int().min(0),
fundedCents: z.number().int().min(0).default(0),
priority: z.number().int().min(0).max(10_000),
dueOn: z.string().datetime(), // ISO
});
const PatchPlan = NewPlan.partial();
const IdParam = z.object({ id: z.string().min(1) });
const bi = (n: number | bigint | undefined) => BigInt(n ?? 0);
function validateFunding(total: bigint, funded: bigint) {
if (funded > total) {
const err: any = new Error("fundedCents must be ≤ totalCents");
err.statusCode = 400;
err.code = "FUNDED_GT_TOTAL";
throw err;
}
}
const plugin: FastifyPluginAsync = async (app) => {
// CREATE
app.post("/api/fixed-plans", async (req, reply) => {
const userId = req.userId;
const parsed = NewPlan.safeParse(req.body);
if (!parsed.success) return reply.status(400).send({ error: "INVALID_BODY", details: parsed.error.flatten() });
const totalBI = bi(parsed.data.totalCents);
const fundedBI = bi(parsed.data.fundedCents);
validateFunding(totalBI, fundedBI);
const rec = await prisma.fixedPlan.create({
data: {
userId,
name: parsed.data.name,
priority: parsed.data.priority,
dueOn: new Date(parsed.data.dueOn),
totalCents: totalBI,
fundedCents: fundedBI,
cycleStart: new Date(), // required by your schema
},
select: { id: true },
});
return reply.status(201).send(rec);
});
// UPDATE
app.patch("/api/fixed-plans/:id", async (req, reply) => {
const userId = req.userId;
const pid = IdParam.safeParse(req.params);
const patch = PatchPlan.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() });
const existing = await prisma.fixedPlan.findFirst({ where: { id: pid.data.id, userId } });
if (!existing) return reply.status(404).send({ error: "NOT_FOUND" });
const nextTotal = patch.data.totalCents !== undefined ? bi(patch.data.totalCents) : (existing.totalCents as bigint);
const nextFunded = patch.data.fundedCents !== undefined ? bi(patch.data.fundedCents) : (existing.fundedCents as bigint);
validateFunding(nextTotal, nextFunded);
await prisma.fixedPlan.update({
where: { id: pid.data.id },
data: {
...(patch.data.name !== undefined ? { name: patch.data.name } : null),
...(patch.data.priority !== undefined ? { priority: patch.data.priority } : null),
...(patch.data.dueOn !== undefined ? { dueOn: new Date(patch.data.dueOn) } : null),
...(patch.data.totalCents !== undefined ? { totalCents: bi(patch.data.totalCents) } : null),
...(patch.data.fundedCents !== undefined ? { fundedCents: bi(patch.data.fundedCents) } : null),
},
});
return reply.send({ ok: true });
});
// DELETE
app.delete("/api/fixed-plans/: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() });
const existing = await prisma.fixedPlan.findFirst({ where: { id: pid.data.id, userId } });
if (!existing) return reply.status(404).send({ error: "NOT_FOUND" });
await prisma.fixedPlan.delete({ where: { id: pid.data.id } });
return reply.send({ ok: true });
});
};
export default plugin;