148 lines
6.3 KiB
TypeScript
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);
|
|
});
|
|
});
|