import { Router } from "express"; import { db, nextId } from "../db.js"; export const productsRouter: Router = Router(); type CreateBody = { sku: string; // strain identity: either link to an existing strain via id, or supply a // name and the server creates / matches one. Strain.name doubles as the // product's display name. strainId?: string | null; strainName?: string; brandId?: string | null; type: string; kind: "bulk" | "discrete"; defaultThc?: number; defaultCbd?: number; defaultTotalCannabinoids?: number; }; productsRouter.post("/products", (req, res) => { const body = req.body as CreateBody; if (!body.sku?.trim()) return res.status(400).json({ error: "sku required" }); if (!body.type) return res.status(400).json({ error: "type required" }); if (body.kind !== "bulk" && body.kind !== "discrete") { return res.status(400).json({ error: "kind must be bulk or discrete" }); } if (!body.strainId && !body.strainName?.trim()) { return res.status(400).json({ error: "strainId or strainName required" }); } const sku = body.sku.trim(); const todayIso = new Date().toISOString().slice(0, 10); const existingSku = db .prepare<[string], { id: string }>("SELECT id FROM products WHERE sku = ?") .get(sku); if (existingSku) { return res.status(409).json({ error: "sku already exists", id: existingSku.id }); } const id = nextId("pdt", "products"); try { const tx = db.transaction(() => { let strainId = body.strainId ?? null; if (!strainId) { const strainName = body.strainName!.trim(); const found = db .prepare<[string], { id: string }>( `SELECT id FROM strains WHERE name = ? COLLATE NOCASE`, ) .get(strainName); if (found) { strainId = found.id; } else { strainId = nextId("str", "strains"); db.prepare( `INSERT INTO strains ( id, name, default_thc, default_cbd, default_total_cannabinoids, created_at ) VALUES (?, ?, ?, ?, ?, ?)`, ).run( strainId, strainName, body.defaultThc ?? null, body.defaultCbd ?? null, body.defaultTotalCannabinoids ?? null, todayIso, ); } } db.prepare( `INSERT INTO products (id, sku, strain_id, brand_id, type, kind, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, ).run(id, sku, strainId, body.brandId ?? null, body.type, body.kind, todayIso); }); tx(); } catch (err) { const msg = err instanceof Error ? err.message : ""; if (msg.includes("UNIQUE")) { return res.status(409).json({ error: "sku already exists" }); } throw err; } res.json({ id }); }); type UpdateBody = Partial<{ sku: string; type: string; kind: "bulk" | "discrete"; strainId: string | null; strainName: string; brandId: string | null; }>; productsRouter.patch("/products/:id", (req, res) => { const { id } = req.params; const body = req.body as UpdateBody; const existing = db .prepare< [string], { id: string; sku: string; type: string; kind: string; strain_id: string; brand_id: string | null } >( `SELECT id, sku, type, kind, strain_id, brand_id FROM products WHERE id = ?`, ) .get(id); if (!existing) return res.status(404).json({ error: "product not found" }); const nextSku = typeof body.sku === "string" && body.sku.trim() ? body.sku.trim() : existing.sku; if (nextSku !== existing.sku) { const duplicate = db .prepare<[string, string], { id: string }>("SELECT id FROM products WHERE sku = ? AND id != ?") .get(nextSku, id); if (duplicate) return res.status(409).json({ error: "sku already exists" }); } const nextType = typeof body.type === "string" && body.type ? body.type : existing.type; const nextKind: "bulk" | "discrete" = body.kind === "bulk" || body.kind === "discrete" ? body.kind : (existing.kind as "bulk" | "discrete"); const nextBrandId = body.brandId === undefined ? existing.brand_id : body.brandId; // Strain handling: prefer explicit strainId; else if strainName given, // match-by-name (case-insensitive) or create a new strain. let nextStrainId = existing.strain_id; const todayIso = new Date().toISOString().slice(0, 10); if (body.strainId !== undefined && body.strainId) { nextStrainId = body.strainId; } else if (typeof body.strainName === "string" && body.strainName.trim()) { const name = body.strainName.trim(); const found = db .prepare<[string], { id: string }>( `SELECT id FROM strains WHERE name = ? COLLATE NOCASE`, ) .get(name); if (found) { nextStrainId = found.id; } else { nextStrainId = nextId("str", "strains"); db.prepare( `INSERT INTO strains (id, name, created_at) VALUES (?, ?, ?)`, ).run(nextStrainId, name, todayIso); } } db.prepare( `UPDATE products SET sku = ?, type = ?, kind = ?, strain_id = ?, brand_id = ? WHERE id = ?`, ).run(nextSku, nextType, nextKind, nextStrainId, nextBrandId, id); res.json({ ok: true }); }); productsRouter.delete("/products/:id", (req, res) => { const { id } = req.params; const inUse = db .prepare<[string], { n: number }>( `SELECT COUNT(*) AS n FROM inventory_items WHERE product_id = ?`, ) .get(id)!.n; if (inUse > 0) { return res .status(409) .json({ error: `product has ${inUse} inventory item${inUse === 1 ? "" : "s"}` }); } const result = db.prepare(`DELETE FROM products WHERE id = ?`).run(id); if (result.changes === 0) return res.status(404).json({ error: "product not found" }); res.json({ ok: true }); }); // Strain management — kept here since strains are catalog cousins of products. productsRouter.patch("/strains/:id", (req, res) => { const { id } = req.params; const body = req.body as Partial<{ name: string; defaultThc: number | null; defaultCbd: number | null; defaultTotalCannabinoids: number | null; notes: string | null; }>; const existing = db .prepare< [string], { id: string; name: string; default_thc: number | null; default_cbd: number | null; default_total_cannabinoids: number | null; notes: string | null; } >( `SELECT id, name, default_thc, default_cbd, default_total_cannabinoids, notes FROM strains WHERE id = ?`, ) .get(id); if (!existing) return res.status(404).json({ error: "strain not found" }); const nextName = typeof body.name === "string" && body.name.trim() ? body.name.trim() : existing.name; const nextThc = body.defaultThc === undefined ? existing.default_thc : body.defaultThc; const nextCbd = body.defaultCbd === undefined ? existing.default_cbd : body.defaultCbd; const nextTotal = body.defaultTotalCannabinoids === undefined ? existing.default_total_cannabinoids : body.defaultTotalCannabinoids; const nextNotes = body.notes === undefined ? existing.notes : body.notes; try { db.prepare( `UPDATE strains SET name = ?, default_thc = ?, default_cbd = ?, default_total_cannabinoids = ?, notes = ? WHERE id = ?`, ).run(nextName, nextThc, nextCbd, nextTotal, nextNotes, id); res.json({ ok: true }); } catch (err) { const msg = err instanceof Error ? err.message : ""; if (msg.includes("UNIQUE")) { return res.status(409).json({ error: "another strain already uses that name" }); } throw err; } });