Files
CapaKraken/packages/api/src/__tests__/report-template-procedure-support.test.ts
T

487 lines
13 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { buildResourceMonthTemplateCompleteness } from "../router/report-blueprints-support.js";
import {
DeleteReportTemplateInputSchema,
deleteReportTemplate,
listReportTemplates,
SaveReportTemplateInputSchema,
saveReportTemplate,
} from "../router/report-template-procedure-support.js";
function createContext(reportTemplate: Record<string, unknown>) {
return {
db: {
reportTemplate,
},
dbUser: {
id: "user_controller",
systemRole: "CONTROLLER",
permissionOverrides: null,
},
} as const;
}
describe("report template procedure support", () => {
it("reuses the shared resource month completeness basis", () => {
expect(buildResourceMonthTemplateCompleteness([
"monthKey",
"displayName",
"countryName",
"federalState",
"metroCityName",
"monthlyPublicHolidayCount",
"monthlyPublicHolidayHoursDeduction",
"monthlyAbsenceDayEquivalent",
"monthlyAbsenceHoursDeduction",
"monthlySahHours",
"monthlyTargetHours",
"monthlyActualBookedHours",
"monthlyUnassignedHours",
])).toMatchObject({
scope: "resource_month",
isAuditReady: true,
isRecommendedComplete: false,
minimumAuditColumnCount: 13,
selectedMinimumAuditColumnCount: 13,
missingMinimumAuditColumns: [],
missingRecommendedColumns: expect.arrayContaining([
"eid",
"chapter",
"monthlyExpectedBookedHours",
]),
});
});
it("lists shared and owned templates with parsed config and ownership flags", async () => {
const updatedAt = new Date("2026-03-31T10:00:00.000Z");
const ctx = createContext({
findMany: vi.fn().mockResolvedValue([
{
id: "tpl_1",
name: "Owned resource month",
description: "Monthly default",
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: ["displayName", "monthlySahHours"],
filters: [],
periodMonth: "2026-03",
},
isShared: false,
ownerId: "user_controller",
updatedAt,
},
{
id: "tpl_2",
name: "Shared project",
description: null,
entity: "PROJECT",
config: {
entity: "project",
columns: ["name"],
filters: [],
},
isShared: true,
ownerId: "user_other",
updatedAt,
},
]),
});
const result = await listReportTemplates(ctx);
expect(ctx.db.reportTemplate.findMany).toHaveBeenCalledWith({
where: {
OR: [
{ ownerId: "user_controller" },
{ isShared: true },
],
},
orderBy: [{ name: "asc" }],
select: {
id: true,
name: true,
description: true,
entity: true,
config: true,
isShared: true,
ownerId: true,
updatedAt: true,
},
});
expect(result).toEqual([
expect.objectContaining({
id: "tpl_1",
name: "Owned resource month",
description: "Monthly default",
entity: "resource_month",
config: {
entity: "resource_month",
columns: ["displayName", "monthlySahHours"],
filters: [],
periodMonth: "2026-03",
sortDir: "asc",
},
isShared: false,
isOwner: true,
completeness: expect.objectContaining({
scope: "resource_month",
isAuditReady: false,
isRecommendedComplete: false,
recommendedColumnCount: 26,
selectedRecommendedColumnCount: 2,
minimumAuditColumnCount: 13,
selectedMinimumAuditColumnCount: 2,
missingRecommendedColumns: expect.arrayContaining([
"monthKey",
"monthlyTargetHours",
"monthlyUnassignedHours",
]),
missingMinimumAuditColumns: expect.arrayContaining([
"monthKey",
"countryName",
"monthlyTargetHours",
]),
}),
updatedAt,
}),
expect.objectContaining({
id: "tpl_2",
name: "Shared project",
description: null,
entity: "project",
config: {
entity: "project",
columns: ["name"],
filters: [],
sortDir: "asc",
},
isShared: true,
isOwner: false,
completeness: null,
updatedAt,
}),
]);
});
it("marks resource month templates audit-ready once the minimum audit basis is present", async () => {
const updatedAt = new Date("2026-03-31T10:30:00.000Z");
const ctx = createContext({
findMany: vi.fn().mockResolvedValue([
{
id: "tpl_audit",
name: "Audit-ready resource month",
description: null,
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: [
"monthKey",
"displayName",
"countryName",
"federalState",
"metroCityName",
"monthlyPublicHolidayCount",
"monthlyPublicHolidayHoursDeduction",
"monthlyAbsenceDayEquivalent",
"monthlyAbsenceHoursDeduction",
"monthlySahHours",
"monthlyTargetHours",
"monthlyActualBookedHours",
"monthlyUnassignedHours",
],
filters: [],
periodMonth: "2026-03",
},
isShared: false,
ownerId: "user_controller",
updatedAt,
},
]),
});
const [result] = await listReportTemplates(ctx);
expect(result?.completeness).toMatchObject({
scope: "resource_month",
isAuditReady: true,
isRecommendedComplete: false,
minimumAuditColumnCount: 13,
selectedMinimumAuditColumnCount: 13,
missingMinimumAuditColumns: [],
missingRecommendedColumns: expect.arrayContaining([
"eid",
"chapter",
"monthlyExpectedBookedHours",
]),
});
});
it("upserts new templates under the current owner with converted entity values", async () => {
const upsert = vi.fn().mockResolvedValue({
id: "tpl_new",
updatedAt: new Date("2026-03-31T11:00:00.000Z"),
});
const ctx = createContext({
upsert,
});
const input = SaveReportTemplateInputSchema.parse({
name: "Monthly default",
description: "Team-wide default",
isShared: true,
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
},
});
const result = await saveReportTemplate(ctx, input);
expect(result).toEqual({
id: "tpl_new",
updatedAt: new Date("2026-03-31T11:00:00.000Z"),
});
expect(upsert).toHaveBeenCalledWith({
where: {
ownerId_name: {
ownerId: "user_controller",
name: "Monthly default",
},
},
update: {
description: "Team-wide default",
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
sortDir: "asc",
},
isShared: true,
},
create: {
ownerId: "user_controller",
name: "Monthly default",
description: "Team-wide default",
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
sortDir: "asc",
},
isShared: true,
},
select: { id: true, updatedAt: true },
});
});
it("updates owned templates and rejects updates for foreign templates", async () => {
const findUnique = vi.fn()
.mockResolvedValueOnce({ ownerId: "user_controller" })
.mockResolvedValueOnce({ ownerId: "user_other" });
const update = vi.fn().mockResolvedValue({
id: "tpl_1",
updatedAt: new Date("2026-03-31T12:00:00.000Z"),
});
const ctx = createContext({
findUnique,
update,
});
const ownedResult = await saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
id: "tpl_1",
name: "Owned template",
description: "Updated",
config: {
entity: "project",
columns: ["name"],
filters: [],
},
}));
expect(ownedResult).toEqual({
id: "tpl_1",
updatedAt: new Date("2026-03-31T12:00:00.000Z"),
});
expect(update).toHaveBeenCalledWith({
where: { id: "tpl_1" },
data: {
name: "Owned template",
description: "Updated",
entity: "PROJECT",
config: {
entity: "project",
columns: ["name"],
filters: [],
sortDir: "asc",
},
isShared: false,
},
select: { id: true, updatedAt: true },
});
await expect(saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
id: "tpl_2",
name: "Foreign template",
config: {
entity: "resource",
columns: ["displayName"],
filters: [],
},
}))).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Template cannot be updated",
});
});
it("clears an existing description when the template is saved with a blank description", async () => {
const findUnique = vi.fn().mockResolvedValue({ ownerId: "user_controller" });
const update = vi.fn().mockResolvedValue({
id: "tpl_1",
updatedAt: new Date("2026-03-31T13:00:00.000Z"),
});
const ctx = createContext({
findUnique,
update,
});
const result = await saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
id: "tpl_1",
name: "Owned template",
description: " ",
config: {
entity: "project",
columns: ["name"],
filters: [],
},
}));
expect(result).toEqual({
id: "tpl_1",
updatedAt: new Date("2026-03-31T13:00:00.000Z"),
});
expect(update).toHaveBeenCalledWith({
where: { id: "tpl_1" },
data: {
name: "Owned template",
description: null,
entity: "PROJECT",
config: {
entity: "project",
columns: ["name"],
filters: [],
sortDir: "asc",
},
isShared: false,
},
select: { id: true, updatedAt: true },
});
});
it("stores blank descriptions as null for new templates", async () => {
const upsert = vi.fn().mockResolvedValue({
id: "tpl_new",
updatedAt: new Date("2026-03-31T14:00:00.000Z"),
});
const ctx = createContext({
upsert,
});
await saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
name: "Monthly default",
description: " ",
isShared: true,
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
},
}));
expect(upsert).toHaveBeenCalledWith({
where: {
ownerId_name: {
ownerId: "user_controller",
name: "Monthly default",
},
},
update: {
description: null,
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
sortDir: "asc",
},
isShared: true,
},
create: {
ownerId: "user_controller",
name: "Monthly default",
description: null,
entity: "RESOURCE_MONTH",
config: {
entity: "resource_month",
columns: ["displayName", "monthlyTargetHours"],
filters: [],
periodMonth: "2026-03",
sortDir: "asc",
},
isShared: true,
},
select: { id: true, updatedAt: true },
});
});
it("maps duplicate template names to a conflict error instead of leaking a raw database error", async () => {
const update = vi.fn().mockRejectedValue({ code: "P2002" });
const findUnique = vi.fn().mockResolvedValue({ ownerId: "user_controller" });
const ctx = createContext({
findUnique,
update,
});
await expect(saveReportTemplate(ctx, SaveReportTemplateInputSchema.parse({
id: "tpl_1",
name: "Monthly default",
config: {
entity: "project",
columns: ["name"],
filters: [],
},
}))).rejects.toMatchObject({
code: "CONFLICT",
message: 'A report template named "Monthly default" already exists',
});
});
it("deletes only owned templates", async () => {
const findUnique = vi.fn()
.mockResolvedValueOnce({ ownerId: "user_controller" })
.mockResolvedValueOnce({ ownerId: "user_other" });
const del = vi.fn().mockResolvedValue(undefined);
const ctx = createContext({
findUnique,
delete: del,
});
const result = await deleteReportTemplate(ctx, DeleteReportTemplateInputSchema.parse({ id: "tpl_1" }));
expect(result).toEqual({ ok: true });
expect(del).toHaveBeenCalledWith({ where: { id: "tpl_1" } });
await expect(deleteReportTemplate(ctx, DeleteReportTemplateInputSchema.parse({ id: "tpl_2" }))).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Template cannot be deleted",
});
});
});