Mirrors the Place-of-Delivery (#19) pattern: an admin clause library that feeds the PO T&C fields as dropdowns. (No "work order" type — POs only, per steer.) - schema + migration: TermsCondition (category enum + text + isActive); the migration seeds the prior TC_DEFAULTS as the starting clauses. - permission manage_terms (Manager + SuperUser + Admin). - admin screen /admin/terms: table + Add/Edit dialogs + activate/deactivate + delete (mirrors /admin/delivery-locations); sidebar link under Administration. - PO forms (new / edit / manager-edit): the five named T&C slots (Delivery / Dispatch / Inspection / Transit Insurance / Payment Terms) become a shared <TermsField> select sourced from active clauses of that category; "Others" stays free text; the fixed boilerplate lines are untouched. - tc* columns stay free-text SNAPSHOTS (export/import unchanged); a current value not among active clauses is preserved as a "(current)" option. - tests: terms CRUD + permission guard + grouping helper (6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
4 KiB
TypeScript
93 lines
4 KiB
TypeScript
/**
|
|
* Integration tests for the Terms & Conditions admin CRUD (issue #11).
|
|
* Covers create/update/toggle/delete + the manage_terms guard, and the
|
|
* grouping helper used to feed the PO T&C dropdowns.
|
|
*/
|
|
import { vi, describe, it, expect, afterAll } from "vitest";
|
|
|
|
vi.mock("@/auth", () => ({ auth: vi.fn() }));
|
|
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
|
|
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import {
|
|
createTerm,
|
|
updateTerm,
|
|
toggleTermActive,
|
|
deleteTerm,
|
|
} from "@/app/(portal)/admin/terms/actions";
|
|
import { getActiveTermsByCategory } from "@/lib/terms-data";
|
|
import { makeSession, fd } from "./helpers";
|
|
|
|
const mockedAuth = vi.mocked(auth);
|
|
const PREFIX = "INTTEST_TERMS_";
|
|
const asManager = () => mockedAuth.mockResolvedValue(makeSession("u-mgr", "MANAGER") as never);
|
|
|
|
afterAll(async () => {
|
|
await db.termsCondition.deleteMany({ where: { text: { startsWith: PREFIX } } });
|
|
});
|
|
|
|
describe("createTerm", () => {
|
|
it("persists a clause under its category", async () => {
|
|
asManager();
|
|
const result = await createTerm(fd({ category: "DELIVERY", text: `${PREFIX}Within 2 days` }));
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
const t = await db.termsCondition.findFirstOrThrow({ where: { text: `${PREFIX}Within 2 days` } });
|
|
expect(t.category).toBe("DELIVERY");
|
|
expect(t.isActive).toBe(true);
|
|
});
|
|
|
|
it("requires text and a valid category", async () => {
|
|
asManager();
|
|
expect("error" in (await createTerm(fd({ category: "DELIVERY", text: " " })))).toBe(true);
|
|
expect("error" in (await createTerm(fd({ category: "NOT_A_CATEGORY", text: "x" })))).toBe(true);
|
|
});
|
|
|
|
it("refuses callers without manage_terms", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await createTerm(fd({ category: "DELIVERY", text: `${PREFIX}nope` }))).toEqual({ error: "Forbidden" });
|
|
mockedAuth.mockResolvedValue(makeSession("u-acc", "ACCOUNTS") as never);
|
|
expect(await createTerm(fd({ category: "DELIVERY", text: `${PREFIX}nope` }))).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|
|
|
|
describe("updateTerm / toggle / delete", () => {
|
|
it("edits, toggles active, then deletes a clause", async () => {
|
|
asManager();
|
|
await createTerm(fd({ category: "PAYMENT_TERMS", text: `${PREFIX}old wording` }));
|
|
const t = await db.termsCondition.findFirstOrThrow({ where: { text: `${PREFIX}old wording` } });
|
|
|
|
expect(await updateTerm(t.id, fd({ category: "INSPECTION", text: `${PREFIX}new wording` }))).toEqual({ ok: true });
|
|
const after = await db.termsCondition.findUniqueOrThrow({ where: { id: t.id } });
|
|
expect(after.text).toBe(`${PREFIX}new wording`);
|
|
expect(after.category).toBe("INSPECTION");
|
|
|
|
expect(await toggleTermActive(t.id)).toEqual({ ok: true });
|
|
expect((await db.termsCondition.findUniqueOrThrow({ where: { id: t.id } })).isActive).toBe(false);
|
|
|
|
expect(await deleteTerm(t.id)).toEqual({ ok: true });
|
|
expect(await db.termsCondition.findUnique({ where: { id: t.id } })).toBeNull();
|
|
});
|
|
|
|
it("guards update/toggle/delete behind the permission", async () => {
|
|
mockedAuth.mockResolvedValue(makeSession("u-tech", "TECHNICAL") as never);
|
|
expect(await updateTerm("x", fd({ category: "DELIVERY", text: "y" }))).toEqual({ error: "Forbidden" });
|
|
expect(await toggleTermActive("x")).toEqual({ error: "Forbidden" });
|
|
expect(await deleteTerm("x")).toEqual({ error: "Forbidden" });
|
|
});
|
|
});
|
|
|
|
describe("getActiveTermsByCategory", () => {
|
|
it("groups only active clauses by category", async () => {
|
|
asManager();
|
|
await createTerm(fd({ category: "DISPATCH", text: `${PREFIX}active dispatch` }));
|
|
await createTerm(fd({ category: "DISPATCH", text: `${PREFIX}inactive dispatch` }));
|
|
const inactive = await db.termsCondition.findFirstOrThrow({ where: { text: `${PREFIX}inactive dispatch` } });
|
|
await toggleTermActive(inactive.id);
|
|
|
|
const map = await getActiveTermsByCategory();
|
|
expect(map.DISPATCH).toContain(`${PREFIX}active dispatch`);
|
|
expect(map.DISPATCH).not.toContain(`${PREFIX}inactive dispatch`);
|
|
});
|
|
});
|