From ee9049e0f70c2fee1bfcd3f83104993f3c4ffd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 22:55:09 +0200 Subject: [PATCH] test(api): lock report template completeness --- .../report-template-procedure-support.test.ts | 205 +++++++++++++++++- 1 file changed, 201 insertions(+), 4 deletions(-) diff --git a/packages/api/src/__tests__/report-template-procedure-support.test.ts b/packages/api/src/__tests__/report-template-procedure-support.test.ts index f284d69..8b98cca 100644 --- a/packages/api/src/__tests__/report-template-procedure-support.test.ts +++ b/packages/api/src/__tests__/report-template-procedure-support.test.ts @@ -79,7 +79,7 @@ describe("report template procedure support", () => { }, }); expect(result).toEqual([ - { + expect.objectContaining({ id: "tpl_1", name: "Owned resource month", description: "Monthly default", @@ -93,9 +93,28 @@ describe("report template procedure support", () => { }, 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, @@ -108,11 +127,65 @@ describe("report template procedure support", () => { }, 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", @@ -235,6 +308,130 @@ describe("report template procedure support", () => { }); }); + 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" })