import { afterEach, describe, expect, it, vi } from "vitest"; import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api"; import { createUpliftRequestKey } from "@/domain/vendor-portal/lib/request-key"; import { validateUpliftAmount } from "@/domain/vendor-portal/lib/uplift-validation"; afterEach(() => { vi.restoreAllMocks(); }); function jsonResponse(data: unknown): Response { return new Response(JSON.stringify({ status: "Success", data }), { status: 200, headers: { "Content-Type": "application/json" }, }); } describe("vendor uplift workflow contract", () => { it("requires reason and evidence only when the requested NTE exceeds the current NTE", () => { expect(validateUpliftAmount("90", 100, "", {})).toBe(""); expect(validateUpliftAmount("110", 100, "", {})).toMatch(/reason/i); expect(validateUpliftAmount("110", 100, "Emergency work", {})).toMatch(/evidence/i); expect(validateUpliftAmount("110", 100, "Emergency work", { hasExisting: true })).toBe(""); }); it("keeps create-mode requests at or below current NTE as NoApprovalRequired", () => { expect(validateUpliftAmount("90", 100, "", { mode: "create" })).toBe(""); expect(validateUpliftAmount("100", 100, "", { mode: "create" })).toBe(""); }); it("rejects revise-mode requests at or below current NTE with an actionable error", () => { expect( validateUpliftAmount("90", 100, "Reason", { hasExisting: true, mode: "revise" }), ).toMatch(/must be greater than the current NTE/i); expect( validateUpliftAmount("100", 100, "Reason", { hasExisting: true, mode: "revise" }), ).toMatch(/must be greater than the current NTE/i); }); it("still requires approval details in revise mode when current NTE is unknown", () => { expect(validateUpliftAmount("110", undefined, "", { mode: "revise" })).toMatch(/reason/i); expect(validateUpliftAmount("110", undefined, "Emergency work", { mode: "revise" })).toMatch( /evidence/i, ); expect( validateUpliftAmount("110", undefined, "Emergency work", { hasExisting: true, mode: "revise", }), ).toBe(""); }); it("uses a secure UUID request key", () => { expect(createUpliftRequestKey()).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, ); }); it("uses dispatch-scoped request, withdraw, revise, and status routes", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({ id: 31 })); await vendorPortalApi.requestUplift("token", 7, { requestedNTE: 125, reason: "Parts", requestKey: "key-1", evidenceDocumentId: 22, }); await vendorPortalApi.withdrawUplift("token", 7, 31); await vendorPortalApi.reviseUplift("token", 7, 31, { requestedNTE: 130, reason: "Updated parts", evidenceDocumentId: 22, }); await vendorPortalApi.getDocumentStatus("token", 7, 22); expect(fetchMock.mock.calls.map(([url]) => new URL(String(url)).pathname)).toEqual([ "/api/vendor-portal/dispatches/7/uplift-request", "/api/vendor-portal/dispatches/7/uplift-request/31/withdraw", "/api/vendor-portal/dispatches/7/uplift-request/31/revise", "/api/vendor-portal/dispatches/7/documents/22/status", ]); expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({ requestedNTE: 125, evidenceDocumentId: 22, }); }); });