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