shoc-frontend-new/src/test/domain/vendor-portal/uplift-workflow.test.ts
2026-08-11 13:49:03 -03:00

86 lines
3.4 KiB
TypeScript

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,
});
});
});