feat(stock-quick-draft): API routes (search, history, add-line, delete-line)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
249a78a7ef
commit
0515b1ce7c
4 changed files with 241 additions and 0 deletions
104
src/app/api/v1/stock-quick-draft/add-line/route.ts
Normal file
104
src/app/api/v1/stock-quick-draft/add-line/route.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||||
import { addQuickDraftLine } from "@/lib/server/stock-quick-draft-service";
|
||||
|
||||
/**
|
||||
* POST /api/v1/stock-quick-draft/add-line
|
||||
*
|
||||
* Adds a single line to a draft stock_purchase for the chosen
|
||||
* supplier. The service creates the draft PO if none exists for
|
||||
* the supplier. Supports inline `newStockItem` payload for the
|
||||
* "+ New SKU" path.
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* supplierId: string,
|
||||
* stockItemId?: string,
|
||||
* quantity: number (>0),
|
||||
* costPrice: number (>=0),
|
||||
* newStockItem?: { name, sku, description?, unit? }
|
||||
* }
|
||||
*
|
||||
* Response (success): { ok: true, purchaseId, purchaseNumber, lineId, createdDraft }
|
||||
*
|
||||
* Error mapping (reason → status):
|
||||
* "validation" → 400
|
||||
* "supplier_not_found" → 404
|
||||
* "stock_item_not_found" → 404
|
||||
* "create_failed" → 500
|
||||
* "append_failed" → 500
|
||||
*/
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const STATUS_BY_REASON: Record<string, number> = {
|
||||
validation: 400,
|
||||
supplier_not_found: 404,
|
||||
stock_item_not_found: 404,
|
||||
create_failed: 500,
|
||||
append_failed: 500,
|
||||
};
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await req.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, reason: "validation", message: "invalid JSON body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const supplierId = typeof body.supplierId === "string" ? body.supplierId : "";
|
||||
const stockItemId =
|
||||
typeof body.stockItemId === "string" ? body.stockItemId : undefined;
|
||||
const quantity = typeof body.quantity === "number" ? body.quantity : NaN;
|
||||
const costPrice = typeof body.costPrice === "number" ? body.costPrice : NaN;
|
||||
const newStockItem =
|
||||
body.newStockItem && typeof body.newStockItem === "object"
|
||||
? (body.newStockItem as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const pb = await getAdminPocketBase();
|
||||
const result = await addQuickDraftLine(pb as never, {
|
||||
supplierId,
|
||||
stockItemId,
|
||||
quantity,
|
||||
costPrice,
|
||||
newStockItem:
|
||||
newStockItem &&
|
||||
typeof newStockItem.name === "string" &&
|
||||
typeof newStockItem.sku === "string"
|
||||
? {
|
||||
name: newStockItem.name,
|
||||
sku: newStockItem.sku,
|
||||
description:
|
||||
typeof newStockItem.description === "string"
|
||||
? newStockItem.description
|
||||
: undefined,
|
||||
unit:
|
||||
typeof newStockItem.unit === "string"
|
||||
? newStockItem.unit
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const status = STATUS_BY_REASON[result.reason] ?? 500;
|
||||
return NextResponse.json(result, { status });
|
||||
}
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
reason: "create_failed",
|
||||
message: err instanceof Error ? err.message : "unknown error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/app/api/v1/stock-quick-draft/line/[lineId]/route.ts
Normal file
46
src/app/api/v1/stock-quick-draft/line/[lineId]/route.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||||
import { removeQuickDraftLine } from "@/lib/server/stock-quick-draft-service";
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/stock-quick-draft/line/[lineId]
|
||||
*
|
||||
* Removes a `stock_purchase_items` row the modal added. Lets the
|
||||
* buyer clean up mistakes without leaving the modal.
|
||||
*
|
||||
* Response (success): { ok: true }
|
||||
* Response (not found): 404 { ok: false, reason: "line_not_found", message }
|
||||
*/
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function DELETE(
|
||||
_req: NextRequest,
|
||||
ctx: { params: Promise<{ lineId: string }> },
|
||||
) {
|
||||
const { lineId } = await ctx.params;
|
||||
if (!lineId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, reason: "validation", message: "lineId is required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = await getAdminPocketBase();
|
||||
const result = await removeQuickDraftLine(pb as never, lineId);
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(result, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
reason: "delete_failed",
|
||||
message: err instanceof Error ? err.message : "unknown error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/app/api/v1/stock-quick-draft/search/route.ts
Normal file
47
src/app/api/v1/stock-quick-draft/search/route.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||||
import { searchStockItemsForQuickDraft } from "@/lib/server/stock-quick-draft-service";
|
||||
|
||||
/**
|
||||
* GET /api/v1/stock-quick-draft/search?q=&limit=
|
||||
*
|
||||
* Typeahead search over `stock_items` for the quick-draft modal.
|
||||
* Admin PB so the result isn't gated on the `listRule`.
|
||||
*
|
||||
* Query params:
|
||||
* - q (required, min 2 chars)
|
||||
* - limit (optional, default 10, max 25)
|
||||
*
|
||||
* Response: { ok: true, items: StockItemSearchResult[] }
|
||||
*/
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = (searchParams.get("q") ?? "").trim();
|
||||
const limitRaw = searchParams.get("limit");
|
||||
const limit = limitRaw ? Number(limitRaw) : undefined;
|
||||
|
||||
if (q.length < 2) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, reason: "missing_query" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = await getAdminPocketBase();
|
||||
const items = await searchStockItemsForQuickDraft(pb as never, q, limit);
|
||||
return NextResponse.json({ ok: true, items });
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
reason: "search_failed",
|
||||
message: err instanceof Error ? err.message : "unknown error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/app/api/v1/stock-quick-draft/supplier-history/route.ts
Normal file
44
src/app/api/v1/stock-quick-draft/supplier-history/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
||||
import { getStockItemSupplierHistory } from "@/lib/server/stock-quick-draft-service";
|
||||
|
||||
/**
|
||||
* GET /api/v1/stock-quick-draft/supplier-history?stockItemId=
|
||||
*
|
||||
* Returns the per-supplier purchase history for a stock_item
|
||||
* (suppliers where we've bought it from before, with last cost +
|
||||
* date). Admin PB so we can read across the stock_purchase_items →
|
||||
* stock_purchases → suppliers chain.
|
||||
*
|
||||
* Response: { ok: true, history: SupplierHistoryEntry[] }
|
||||
* Returns empty array when no history.
|
||||
*/
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const stockItemId = searchParams.get("stockItemId") ?? "";
|
||||
|
||||
if (!stockItemId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, reason: "missing_stock_item_id" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = await getAdminPocketBase();
|
||||
const history = await getStockItemSupplierHistory(pb as never, stockItemId);
|
||||
return NextResponse.json({ ok: true, history });
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
reason: "history_failed",
|
||||
message: err instanceof Error ? err.message : "unknown error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue