WFDASH-WEBAPP/src/lib/server/order-item-context-service.test.ts
2026-08-07 02:42:57 +02:00

148 lines
6.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { getOrderItemContext } from "@/lib/server/order-item-context-service";
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
vi.mock("@/lib/server/pocketbase-admin", () => ({
getAdminPocketBase: vi.fn(),
}));
interface CollectionMock {
getOne: ReturnType<typeof vi.fn>;
getList: ReturnType<typeof vi.fn>;
}
function makePb(collections: Record<string, CollectionMock>) {
return {
collection: (name: string) => {
const c = collections[name];
if (!c) throw new Error(`unexpected collection: ${name}`);
return c;
},
};
}
const ORD = "order_items";
const Q = "quote_items";
const STK = "stock_items";
const SUP = "suppliers";
describe("getOrderItemContext", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns all-null context when the target has no SKU and no description", async () => {
const pb = makePb({
[ORD]: { getOne: vi.fn().mockResolvedValue({ id: "x", sku: "", description: "" }), getList: vi.fn() },
});
vi.mocked(getAdminPocketBase).mockResolvedValue(pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
const ctx = await getOrderItemContext("x");
expect(ctx.last_quote).toBeNull();
expect(ctx.last_order).toBeNull();
expect(ctx.last_invoiced).toBeNull();
expect(ctx.stock).toBeNull();
expect(ctx.suppliers).toEqual([]);
expect(ctx.cheapest).toBeNull();
expect(ctx.match).toEqual({ sku: "", match_kind: "none", match_confidence: 0 });
});
it("populates last_quote from the SKU-based quote_items join", async () => {
const target = { id: "x", sku: "DRL-6MM", description: "Drill 6mm", quote_item: "qi0" };
const quoteList = {
items: [{
id: "qi1",
quote: "q1",
cost_price: 100,
selling_price: 150,
created: "2026-08-01T10:00:00Z",
expand: { quote: { quote_number: "Q-26080101", status: "approved", customer_name: "Acme", company: "Acme Ltd" } },
}],
};
const emptyList = { items: [] };
const supplierList = { items: [{ id: "sup1", suppliername: "Fastener Co" }] };
const pb = makePb({
[ORD]: { getOne: vi.fn().mockResolvedValue(target), getList: vi.fn().mockResolvedValue(emptyList) },
[Q]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(quoteList) },
[STK]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(emptyList) },
[SUP]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(supplierList) },
});
vi.mocked(getAdminPocketBase).mockResolvedValue(pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
const ctx = await getOrderItemContext("x");
expect(ctx.match).toEqual({ sku: "DRL-6MM", match_kind: "sku_exact", match_confidence: 1 });
expect(ctx.last_quote?.quote_number).toBe("Q-26080101");
expect(ctx.last_quote?.cost_price).toBe(100);
expect(ctx.last_quote?.selling_price).toBe(150);
expect(ctx.last_quote?.markup_percent).toBe(50);
});
it("marks stock status as 'low' when current_qty <= min_qty and > 0", async () => {
const target = { id: "x", sku: "DRL-6MM", description: "Drill", quote_item: "" };
const emptyList = { items: [] };
const supplierList = { items: [] };
const stockList = {
items: [{ id: "s1", name: "Drill 6mm", sku: "DRL-6MM", current_qty: 5, min_qty: 10, selling_price: 140, markup: 40 }],
};
const pb = makePb({
[ORD]: { getOne: vi.fn().mockResolvedValue(target), getList: vi.fn().mockResolvedValue(emptyList) },
[Q]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(emptyList) },
[STK]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(stockList) },
[SUP]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(supplierList) },
});
vi.mocked(getAdminPocketBase).mockResolvedValue(pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
const ctx = await getOrderItemContext("x");
expect(ctx.stock?.status).toBe("low");
});
it("marks stock status as 'out' when current_qty <= 0", async () => {
const target = { id: "x", sku: "DRL-6MM", description: "Drill", quote_item: "" };
const emptyList = { items: [] };
const supplierList = { items: [] };
const stockList = {
items: [{ id: "s1", name: "Drill 6mm", sku: "DRL-6MM", current_qty: 0, min_qty: 5, selling_price: 140, markup: 40 }],
};
const pb = makePb({
[ORD]: { getOne: vi.fn().mockResolvedValue(target), getList: vi.fn().mockResolvedValue(emptyList) },
[Q]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(emptyList) },
[STK]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(stockList) },
[SUP]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(supplierList) },
});
vi.mocked(getAdminPocketBase).mockResolvedValue(pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
const ctx = await getOrderItemContext("x");
expect(ctx.stock?.status).toBe("out");
});
it("populates last_invoiced when an invoiced row exists", async () => {
const target = { id: "x", sku: "DRL-6MM", description: "Drill", quote_item: "" };
const emptyList = { items: [] };
const supplierList = { items: [] };
const invoicedList = {
items: [{
id: "oi2",
order: "o1",
supplier_invoice_number: "INV-99",
customer_invoice_number: "C-INV-7",
invoiced_date: "2026-08-02T10:00:00Z",
cost_price: 105,
quantity: 10,
qty_invoiced: 10,
expand: { order: { order_number: "ORD-26080201" } },
}],
};
const pb = makePb({
[ORD]: { getOne: vi.fn().mockResolvedValue(target), getList: vi.fn((_n: string, _p: unknown, opts: { sort?: string }) => opts?.sort === "-invoiced_date" ? invoicedList : emptyList) },
[Q]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(emptyList) },
[STK]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(emptyList) },
[SUP]: { getOne: vi.fn(), getList: vi.fn().mockResolvedValue(supplierList) },
});
vi.mocked(getAdminPocketBase).mockResolvedValue(pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>);
const ctx = await getOrderItemContext("x");
expect(ctx.last_invoiced?.supplier_invoice_number).toBe("INV-99");
expect(ctx.last_invoiced?.cost_price).toBe(105);
});
});