import type { Report } from "@prisma/client"; import { z } from "zod"; import { emitEvent } from "@/server/events"; import { can, ServiceError, type ServiceCtx } from "@/server/services/context"; // TODO(merge L2): import from "@/server/services/work-orders/transition" import { transitionWorkOrder } from "@/server/services/work-orders/transition"; import { auditReport, contentOf, orderNumberOf, reportAuditView, requireVisibleReport } from "./common"; export const rejectReportSchema = z.object({ reportId: z.string().min(1).max(64), reason: z.string().trim().min(3).max(2000), }); export type RejectReportInput = z.input; /** Zurückweisen mit Pflichtgrund: report → rejected, completion order in_review → in_progress (Korrektur). */ export async function rejectReport(ctx: ServiceCtx, raw: RejectReportInput): Promise { const final = can(ctx, "report:approve"); if (!final && !can(ctx, "report:approve_team")) throw new ServiceError("forbidden", "missing permission report:approve"); const parsed = rejectReportSchema.safeParse(raw); if (!parsed.success) throw new ServiceError("invalid", "reason required", { field: "reason" }); const input = parsed.data; const report = await requireVisibleReport(ctx, input.reportId); const from = final ? (["submitted", "team_approved"] as const) : (["submitted"] as const); if (!(from as readonly string[]).includes(report.status)) throw new ServiceError("conflict", `report is ${report.status}`); const res = await ctx.db.report.updateMany({ where: { id: report.id, status: { in: [...from] } }, data: { status: "rejected", rejectionReason: input.reason }, }); if (res.count !== 1) throw new ServiceError("conflict", "report changed concurrently"); const updated = await ctx.db.report.findFirstOrThrow({ where: { id: report.id } }); // a report can be rejected more than once → one occurrence per rejection (L6 mail deduplication) const occurrenceId = `${report.id}:rejected:${updated.updatedAt.getTime()}`; if (report.type === "completion") { const wo = await ctx.db.workOrder.findFirstOrThrow({ where: { id: report.workOrderId }, select: { status: true } }); if (wo.status === "in_review") await transitionWorkOrder(ctx, { workOrderId: report.workOrderId, to: "in_progress", reason: input.reason, eventData: { occurrenceId } }); } await auditReport(ctx, "update", report.id, reportAuditView(report), reportAuditView(updated)); await emitEvent(ctx, { type: "report.rejected", entityType: "report", entityId: report.id, data: { reportType: report.type, number: contentOf(updated).reportNumber, workOrderNumber: await orderNumberOf(ctx, report.workOrderId), reason: input.reason, occurrenceId }, }); return updated; }