import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { connectDb } from "@/lib/server/db"; import { Branch } from "@/lib/server/models/Branch"; import { Product } from "@/lib/server/models/Product"; import { StockMovement } from "@/lib/server/models/StockMovement"; import { authorize, MANAGERS } from "@/lib/server/auth"; import { objectId } from "@/lib/server/validation"; import { clientRefId, recordMovement, stockOf } from "@/lib/server/stock"; export const runtime = "nodejs"; const adjustmentSchema = z.object({ clientUuid: z.string().uuid(), branchId: objectId.optional(), // owners must give one; managers default to their own branch productId: objectId, // COUNT: "we physically counted N", so the ledger is corrected to N. // DELTA: "add or remove N", e.g. opening stock, breakage, or a delivery // that was not recorded as a purchase. mode: z.enum(["COUNT", "DELTA"]), quantity: z.number().int().min(-1_000_000).max(1_000_000), note: z.string().trim().max(300).optional(), }); // POST /api/stock/adjustments — managers (own branch) and owners (any branch). export async function POST(req: NextRequest) { await connectDb(); const gate = authorize(req, MANAGERS); if (!gate.ok) return gate.response; const auth = gate.auth; const parsed = adjustmentSchema.safeParse(await req.json().catch(() => null)); if (!parsed.success) { return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 }); } const data = parsed.data; if (data.mode === "COUNT" && data.quantity < 0) { return NextResponse.json({ error: "A counted quantity cannot be negative" }, { status: 400 }); } let branchId: string | null; if (auth.role === "SUPER_ADMIN") { branchId = data.branchId ?? null; if (!branchId) return NextResponse.json({ error: "branchId is required" }, { status: 400 }); } else { branchId = auth.branchId; if (data.branchId && data.branchId !== auth.branchId) { return NextResponse.json({ error: "Cannot adjust stock for another branch" }, { status: 403 }); } } if (!branchId) return NextResponse.json({ error: "No branch assignment for this account" }, { status: 403 }); const [branch, product] = await Promise.all([ Branch.exists({ _id: branchId, businessId: auth.businessId }), Product.exists({ _id: data.productId, businessId: auth.businessId }), ]); if (!branch) return NextResponse.json({ error: "Branch not found in your business" }, { status: 404 }); if (!product) return NextResponse.json({ error: "Product not found in your business" }, { status: 404 }); // A retried submit with the same clientUuid is a no-op. const refId = clientRefId(auth.businessId, data.clientUuid); const already = await StockMovement.exists({ type: "ADJUSTMENT", refId }); if (!already) { const before = await stockOf(auth.businessId, branchId, data.productId); const delta = data.mode === "COUNT" ? data.quantity - before : data.quantity; if (delta !== 0) { const label = data.mode === "COUNT" ? `Count: ${before} -> ${data.quantity}` : "Adjustment"; await recordMovement({ businessId: auth.businessId, branchId, productId: data.productId, type: "ADJUSTMENT", quantity: delta, refId, note: data.note ? `${label}. ${data.note}` : label, recordedBy: auth.sub, }); } } const quantity = await stockOf(auth.businessId, branchId, data.productId); return NextResponse.json({ branchId, productId: data.productId, quantity }, { status: 201 }); }