import { describe, it, expect, vi, beforeEach } from "vitest"; import { getAdminPocketBase } from "@/lib/server/pocketbase-admin"; import { createCustomerQuoteFromRepair } from "@/lib/server/repairs-service"; import { createQuote } from "@/lib/server/quotes-service"; vi.mock("@/lib/server/pocketbase-admin", () => ({ getAdminPocketBase: vi.fn(), })); vi.mock("@/lib/server/quotes-service", () => ({ // Only `createQuote` is needed by the function under test. createQuote: vi.fn(), })); const REPAIRS = "repairs"; interface CollectionMock { getOne?: ReturnType; update?: ReturnType; } const baseRepair = { id: "rep1", repair_number: "REP-2026-09-001", customer: "cust1", company: "co1", order: "", supplier: "", status: "quoted", item_description: "Hydraulic ram", fault_description: "Seal leak", notes: "", serial_no: "SN-12345", photos: [], quote: ["supplier-quote.pdf"], purchase_order: [], repair_request: [], }; function makePb(repair: typeof baseRepair & Record) { const getOne = vi.fn().mockImplementation(async (id: string) => { if (id !== repair.id) { const err = new Error("not found") as Error & { status?: number }; err.status = 404; throw err; } return repair; }); const update = vi .fn() .mockImplementation(async (id: string, patch: Record) => ({ ...repair, id, ...patch, })); return { autoCancellation: vi.fn(), collection: (name: string) => { if (name !== REPAIRS) { throw new Error(`unexpected collection: ${name}`); } return { getOne, update } satisfies CollectionMock; }, _getOne: getOne, _update: update, }; } function mockAdminPb(pb: ReturnType) { vi.mocked(getAdminPocketBase).mockResolvedValue( pb as unknown as Awaited>, ); } describe("createCustomerQuoteFromRepair", () => { beforeEach(() => vi.clearAllMocks()); it("creates a quote from the repair context and patches the back-reference", async () => { const pb = makePb(baseRepair); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "quote-new", quote_number: "Q-2026-09-099" } as never, customer: { id: "cust1", customer: "Acme Mining" } as never, company: { id: "co1", company: "Acme Group" } as never, items: [], quote_number: "Q-2026-09-099", }); const result = await createCustomerQuoteFromRepair("rep1", { items: [ { description: "Replace seal kit", quantity: 1, cost_price: 1200, markup: 25, }, ], }); // createQuote received the right customer/company/description and // did NOT get a status override (defaults to "new"). expect(createQuote).toHaveBeenCalledWith( expect.objectContaining({ customer: "cust1", company: "co1", description: "Repair of Hydraulic ram (S/N SN-12345)", items: [ expect.objectContaining({ description: "Replace seal kit" }), ], }), ); // No status override was passed. const passedStatus = vi.mocked(createQuote).mock.calls[0][0].status; expect(passedStatus).toBeUndefined(); // Back-reference is patched on the repair. expect(pb._update).toHaveBeenCalledWith("rep1", { customer_quote: "quote-new", }); // Return payload surfaces the new quote + refreshed repair. expect(result.quote.id).toBe("quote-new"); expect(result.customer.id).toBe("cust1"); expect(result.repair.customer_quote).toBe("quote-new"); }); it("omits the serial-number suffix when serial_no is empty", async () => { const pb = makePb({ ...baseRepair, serial_no: "" }); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "quote-new" } as never, customer: { id: "cust1" } as never, company: null, items: [], quote_number: "Q-x", }); await createCustomerQuoteFromRepair("rep1", { items: [{ description: "Strip & quote" }], }); const description = vi.mocked(createQuote).mock.calls[0][0].description; expect(description).toBe("Repair of Hydraulic ram"); expect(description).not.toMatch(/S\/N/); }); it("respects an operator-supplied description override", async () => { const pb = makePb(baseRepair); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "quote-new" } as never, customer: { id: "cust1" } as never, company: null, items: [], quote_number: "Q-x", }); await createCustomerQuoteFromRepair("rep1", { items: [{ description: "x" }], description: "Custom subject for the email", }); expect(vi.mocked(createQuote).mock.calls[0][0].description).toBe( "Custom subject for the email", ); }); it("forwards quote_notes and validity_days when supplied", async () => { const pb = makePb(baseRepair); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "quote-new" } as never, customer: { id: "cust1" } as never, company: null, items: [], quote_number: "Q-x", }); await createCustomerQuoteFromRepair("rep1", { items: [{ description: "x" }], quote_notes: "Includes collection + return", validity_days: 14, }); const passed = vi.mocked(createQuote).mock.calls[0][0]; expect(passed.quote_notes).toBe("Includes collection + return"); expect(passed.validity_days).toBe(14); }); it("rejects when the repair has no customer", async () => { const pb = makePb({ ...baseRepair, customer: "" }); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "x" } as never, customer: { id: "x" } as never, company: null, items: [], quote_number: "x", }); await expect( createCustomerQuoteFromRepair("rep1", { items: [{ description: "x" }], }), ).rejects.toThrow(/no customer set/); // Did not call createQuote, did not patch the repair. expect(createQuote).not.toHaveBeenCalled(); expect(pb._update).not.toHaveBeenCalled(); }); it("rejects when an existing customer_quote link is set and replace is not", async () => { const pb = makePb({ ...baseRepair, customer_quote: "quote-old" }); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "x" } as never, customer: { id: "x" } as never, company: null, items: [], quote_number: "x", }); await expect( createCustomerQuoteFromRepair("rep1", { items: [{ description: "x" }], }), ).rejects.toThrow(/already has a customer quote/); expect(createQuote).not.toHaveBeenCalled(); }); it("allows overwriting the back-reference when replace=true", async () => { const pb = makePb({ ...baseRepair, customer_quote: "quote-old" }); mockAdminPb(pb); vi.mocked(createQuote).mockResolvedValue({ quote: { id: "quote-new" } as never, customer: { id: "cust1" } as never, company: null, items: [], quote_number: "Q-new", }); const result = await createCustomerQuoteFromRepair("rep1", { items: [{ description: "x" }], replace: true, }); expect(pb._update).toHaveBeenCalledWith("rep1", { customer_quote: "quote-new", }); expect(result.repair.customer_quote).toBe("quote-new"); }); it("rejects when items is empty", async () => { const pb = makePb(baseRepair); mockAdminPb(pb); await expect( createCustomerQuoteFromRepair("rep1", { items: [] }), ).rejects.toThrow(/items must be a non-empty array/); }); });