import { beforeEach, describe, expect, it, vi } from "vitest"; import { API_PATHS } from "@/api/api-paths"; const apiGet = vi.fn(); const apiPost = vi.fn(); const apiPut = vi.fn(); const apiDelete = vi.fn(); vi.mock("@/api/api", () => ({ apiGet: (...args: unknown[]) => apiGet(...args), apiPost: (...args: unknown[]) => apiPost(...args), apiPut: (...args: unknown[]) => apiPut(...args), apiDelete: (...args: unknown[]) => apiDelete(...args), })); import { vendorsApi } from "@/domain/vendors/api/vendors-api"; describe("vendorsApi.getList", () => { beforeEach(() => { apiGet.mockReset(); apiPost.mockReset(); apiPut.mockReset(); apiDelete.mockReset(); apiGet.mockResolvedValue({ Data: [ { Id: 7, CompanyId: "co-1", CompanyName: "Gateway Plumbing", ContactName: "Adam Whyte", Email: "dispatch@gateway.test", Phone: "314-555-0198", Address: "1 Market St", City: "St. Louis", State: "MO", IsActive: false, PreferredContact: "Email", Technicians: [ { Id: 7, ContactName: "Adam Whyte", Email: "adam@gateway.test", Phone: "314-555-0198", TradeSpecialties: "Plumbing", IsActive: false, TotalJobs: 3, }, ], }, ], TotalCount: 1, }); }); it("forwards the status filter and maps list location fields", async () => { const result = await vendorsApi.getList({ search: "gateway", isActive: false, page: 1 }); expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getList, { searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 }, }); expect(result.items[0]).toMatchObject({ address: "1 Market St", city: "St. Louis", state: "MO", isActive: false, companyId: "co-1", preferredContact: "Email", technicians: [ expect.objectContaining({ id: 7, email: "adam@gateway.test", phone: "314-555-0198", totalJobs: 3, }), ], }); }); it("synthesizes one technician for a legacy ungrouped response", async () => { apiGet.mockResolvedValueOnce({ Data: [ { Id: 9, CompanyName: "Legacy Co", ContactName: "Legacy Tech", Email: "legacy@example.test", Phone: "555-0109", IsActive: true, }, ], TotalCount: 1, }); const result = await vendorsApi.getList(); expect(result.items[0]?.technicians).toEqual([ expect.objectContaining({ id: 9, contactName: "Legacy Tech", phone: "555-0109" }), ]); }); it("forwards all selected facets as repeated indexed query parameters", async () => { await vendorsApi.getList({ companies: ["Gateway Plumbing", "Metro HVAC"], trades: ["Plumbing"], locations: ["St. Louis, MO"], jobBuckets: ["50-99"], }); expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getList, { searchParams: { search: "", page: 1, pageSize: 12, "companies[0]": "Gateway Plumbing", "companies[1]": "Metro HVAC", "trades[0]": "Plumbing", "locations[0]": "St. Louis, MO", "jobBuckets[0]": "50-99", }, }); }); it("maps company profiles and labels returned by the facets endpoint", async () => { apiGet.mockResolvedValueOnce({ data: { companies: [ { name: "Gateway Plumbing", companyPhone: "314-555-0100", email: "dispatch@gateway.test", address: "1 Market St", city: "St. Louis", state: "MO", zip: "63101", googleMapsUrl: "https://maps.google.com/gateway", }, ], trades: ["Plumbing"], locations: [{ city: "St. Louis", state: "MO" }], jobBuckets: [{ id: "50-99", label: "50–99" }], }, }); const facets = await vendorsApi.getFacets(); expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.facets, { searchParams: {} }); expect(facets.companies[0]).toMatchObject({ name: "Gateway Plumbing", companyPhone: "314-555-0100", googleMapsUrl: "https://maps.google.com/gateway", }); expect(facets.locations[0]?.label).toBe("St. Louis, MO"); expect(facets.jobBuckets[0]).toEqual({ id: "50-99", label: "50–99" }); }); it("serializes the requested facet scope as an isActive query parameter", async () => { apiGet.mockResolvedValue({ data: { companies: [], trades: [], locations: [], jobBuckets: [] }, }); await vendorsApi.getFacets({ isActive: true }); await vendorsApi.getFacets({ isActive: false }); expect(apiGet).toHaveBeenNthCalledWith(1, API_PATHS.vendor.facets, { searchParams: { isActive: "true" }, }); expect(apiGet).toHaveBeenNthCalledWith(2, API_PATHS.vendor.facets, { searchParams: { isActive: "false" }, }); }); it("sends all editable prototype fields when creating and updating", async () => { const values = { companyId: "42", companyName: "Gateway Plumbing", contactName: "Adam Whyte", phone: "+1 314-555-0198", companyPhone: "314-555-0100", email: "dispatch@gateway.test", address: "1 Market St", city: "St. Louis", state: "MO", zip: "63101", tradeSpecialties: "Plumbing, HVAC", googleMapsUrl: "https://maps.google.com/gateway", notes: "Preferred after-hours vendor", totalJobs: 72, isActive: true, }; apiPost.mockResolvedValueOnce({ data: values }); apiPut.mockResolvedValueOnce({ data: { ...values, isActive: false } }); await vendorsApi.create(values); await vendorsApi.update(7, { ...values, isActive: false }); expect(apiPost).toHaveBeenCalledWith( API_PATHS.rest.vendors, expect.objectContaining({ companyName: "Gateway Plumbing", notes: values.notes, companyId: 42, phone: "(314) 555-0198", companyPhone: "(314) 555-0100", }), ); expect(apiPost.mock.calls[0]?.[1]).not.toHaveProperty("preferredContact"); expect(apiPut).toHaveBeenCalledWith( `${API_PATHS.rest.vendors}/7`, expect.objectContaining({ companyName: "Gateway Plumbing", isActive: false }), ); expect(apiPut.mock.calls[0]?.[1]).not.toHaveProperty("preferredContact"); expect(apiPost.mock.calls[0]?.[1]).not.toHaveProperty("totalJobs"); expect(apiPut.mock.calls[0]?.[1]).not.toHaveProperty("totalJobs"); }); it("sends a null companyId when free-text company has no company link", async () => { apiPost.mockResolvedValueOnce({ data: {} }); await vendorsApi.create({ companyName: "Independent Vendor LLC", contactName: "Indy Tech", }); expect(apiPost).toHaveBeenCalledWith( API_PATHS.rest.vendors, expect.objectContaining({ companyName: "Independent Vendor LLC", companyId: null }), ); }); it("does not send a non-numeric company id to the integer backend contract", async () => { apiPost.mockResolvedValueOnce({ data: {} }); await vendorsApi.create({ companyId: "co-1", companyName: "Gateway Plumbing", contactName: "Adam Whyte", }); expect(apiPost).toHaveBeenCalledWith( API_PATHS.rest.vendors, expect.objectContaining({ companyId: null }), ); }); it("preserves a legacy phone on read and omits it from an unrelated update", async () => { apiGet.mockResolvedValueOnce({ data: { id: 7, companyName: "Legacy Vendor", contactName: "Taylor", phone: "555-1234 ext 9", }, }); apiPut.mockResolvedValueOnce({ data: {} }); const vendor = await vendorsApi.getById(7); expect(vendor.phone).toBe("555-1234 ext 9"); await vendorsApi.update(7, { notes: "Updated only the notes" }); expect(apiPut).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`, { notes: "Updated only the notes", }); }); it("sends only changed fields and canonicalizes a supplied phone", async () => { apiPut.mockResolvedValueOnce({ data: {} }); await vendorsApi.update(7, { isActive: false, companyId: null, phone: "314-555-0198", }); expect(apiPut).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`, { isActive: false, companyId: null, phone: "(314) 555-0198", }); }); it("maps the deactivation-impact payload including linked work orders", async () => { apiGet.mockResolvedValueOnce({ data: { vendorId: 7, canDeactivate: false, openWorkOrders: [ { workOrderId: 101, workOrderNumber: "WO-101", workOrderTitle: "Boiler repair", status: "Dispatched", scheduledDate: "2025-01-02", dispatchId: 501, }, ], }, }); const impact = await vendorsApi.getDeactivationImpact(7); expect(apiGet).toHaveBeenCalledWith( `${API_PATHS.rest.vendors}/7/${API_PATHS.vendor.deactivationImpact}`, ); expect(impact).toEqual({ vendorId: 7, canDeactivate: false, openWorkOrders: [ { workOrderId: 101, workOrderNumber: "WO-101", workOrderTitle: "Boiler repair", status: "Dispatched", scheduledDate: "2025-01-02", dispatchId: 501, }, ], }); }); it("deletes a vendor by id and surfaces the 409 error without retry swallowing", async () => { apiDelete.mockResolvedValueOnce(undefined); await vendorsApi.delete(7); expect(apiDelete).toHaveBeenCalledWith(`${API_PATHS.rest.vendors}/7`); const conflict = Object.assign(new Error("Conflict"), { response: { status: 409 } }); apiDelete.mockRejectedValueOnce(conflict); await expect(vendorsApi.delete(8)).rejects.toThrow("Conflict"); }); }); describe("vendorsApi.getDirectoryList", () => { beforeEach(() => { apiGet.mockReset(); apiGet.mockResolvedValue({ Data: [ { Id: 7, CompanyId: "co-1", CompanyName: "Gateway Plumbing", ContactName: "Adam Whyte", Email: "dispatch@gateway.test", Phone: "314-555-0198", Address: "1 Market St", City: "St. Louis", State: "MO", IsActive: false, PreferredContact: "Email", Technicians: [ { Id: 7, ContactName: "Adam Whyte", Email: "adam@gateway.test", Phone: "314-555-0198", TradeSpecialties: "Plumbing", IsActive: false, TotalJobs: 3, }, ], }, ], TotalCount: 1, }); }); it("targets the dedicated directory endpoint while getList keeps the legacy route", async () => { await vendorsApi.getDirectoryList({ search: "gateway", isActive: false, page: 1 }); await vendorsApi.getList({ search: "gateway", isActive: false, page: 1 }); expect(API_PATHS.vendor.getDirectoryList).toBe("Vendor/GetVendorDirectoryList"); expect(API_PATHS.vendor.getList).toBe("Vendor/GetVendorList"); expect(apiGet).toHaveBeenNthCalledWith(1, API_PATHS.vendor.getDirectoryList, { searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 }, }); expect(apiGet).toHaveBeenNthCalledWith(2, API_PATHS.vendor.getList, { searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 }, }); }); it("builds directory query parameters with the same repeated facet encoding", async () => { await vendorsApi.getDirectoryList({ companies: ["Gateway Plumbing", "Metro HVAC"], trades: ["Plumbing"], locations: ["St. Louis, MO"], jobBuckets: ["50-99"], }); expect(apiGet).toHaveBeenCalledWith(API_PATHS.vendor.getDirectoryList, { searchParams: { search: "", page: 1, pageSize: 12, "companies[0]": "Gateway Plumbing", "companies[1]": "Metro HVAC", "trades[0]": "Plumbing", "locations[0]": "St. Louis, MO", "jobBuckets[0]": "50-99", }, }); }); it("maps grouped company rows with the shared grouped-row mapper", async () => { const result = await vendorsApi.getDirectoryList({ isActive: false, page: 1 }); expect(result.items[0]).toMatchObject({ companyId: "co-1", companyName: "Gateway Plumbing", address: "1 Market St", city: "St. Louis", state: "MO", isActive: false, technicians: [ expect.objectContaining({ id: 7, contactName: "Adam Whyte", email: "adam@gateway.test", phone: "314-555-0198", totalJobs: 3, }), ], }); }); });