WFDASH-WEBAPP/src/lib/server/repairs-service.test.ts
Dave d8e8714720 feat(stock): update pricing logic and transform script for fasteners
- Enhanced `nss-fasteners-pb-import.json` to compute `selling_price` as `cost_price * (1 + 80%)` and included `markup` in the records.
- Updated `nss-fasteners-pb-transform.ts` to allow configurable CSV and output file names via environment variables, improving flexibility for different product runs.
- Added support for uploading photos in the repair detail page, including validation for file types and sizes, and implemented a dialog for creating customer quotes linked to repairs.
- Introduced a new `customer_quote` field in the `Repair` type to link generated quotes back to their respective repairs.

These changes streamline the pricing process and enhance the user experience in managing repairs and quotes.
2026-09-21 12:32:44 +02:00

262 lines
7.6 KiB
TypeScript

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<typeof vi.fn>;
update?: ReturnType<typeof vi.fn>;
}
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<string, unknown>) {
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<string, unknown>) => ({
...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<typeof makePb>) {
vi.mocked(getAdminPocketBase).mockResolvedValue(
pb as unknown as Awaited<ReturnType<typeof getAdminPocketBase>>,
);
}
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/);
});
});