From 6f8c6da0fef12486452f1d4af1bf139ad2dd0c6e Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 16:54:03 +0200 Subject: [PATCH] feat(stock): add POST /api/v1/stock/items/set-pricing --- .../api/v1/stock/items/set-pricing/route.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/app/api/v1/stock/items/set-pricing/route.ts diff --git a/src/app/api/v1/stock/items/set-pricing/route.ts b/src/app/api/v1/stock/items/set-pricing/route.ts new file mode 100644 index 0000000..47d410d --- /dev/null +++ b/src/app/api/v1/stock/items/set-pricing/route.ts @@ -0,0 +1,78 @@ +import { NextResponse } from "next/server"; +import { updateStockItemsPricing } from "@/lib/server/stock-pricing-service"; +import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; + +const MAX_UPDATES = 100; + +const PB_ID_RE = /^[a-z0-9]{15}$/; + +type Body = { + updates?: Array<{ id?: unknown; costPrice?: unknown; sellingPrice?: unknown; markup?: unknown }>; + actor?: { id?: unknown; name?: unknown }; +}; + +export async function POST(req: Request) { + let body: Body; + try { + body = (await req.json()) as Body; + } catch { + return NextResponse.json( + { code: "invalid_json", message: "Body is not valid JSON" }, + { status: 400 }, + ); + } + + if (!Array.isArray(body.updates) || body.updates.length === 0) { + return NextResponse.json( + { code: "validation_error", message: "updates must be a non-empty array" }, + { status: 400 }, + ); + } + if (body.updates.length > MAX_UPDATES) { + return NextResponse.json( + { + code: "validation_error", + message: `At most ${MAX_UPDATES} updates per call`, + }, + { status: 400 }, + ); + } + if (!body.actor || typeof body.actor.id !== "string" || !body.actor.id) { + return NextResponse.json( + { code: "validation_error", message: "actor.id is required" }, + { status: 400 }, + ); + } + + const updates = body.updates.map((u, i) => { + if (typeof u.id !== "string" || !PB_ID_RE.test(u.id)) { + throw new Error(`updates[${i}].id must be a 15-char PB id`); + } + const out: { id: string; costPrice?: number; sellingPrice?: number; markup?: number } = { + id: u.id, + }; + if (u.costPrice !== undefined) out.costPrice = Number(u.costPrice); + if (u.sellingPrice !== undefined) out.sellingPrice = Number(u.sellingPrice); + if (u.markup !== undefined) out.markup = Number(u.markup); + return out; + }); + + let pb; + try { + pb = await getAdminPocketBase(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return NextResponse.json( + { code: "pb_error", message }, + { status: 500 }, + ); + } + + const result = await updateStockItemsPricing({ + pb, + updates, + actor: { id: body.actor.id, name: String(body.actor.name ?? body.actor.id) }, + }); + + return NextResponse.json(result, { status: 200 }); +}