From b554c8e4366b37d27b08fc03b6cdfbb499879781 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 24 Apr 2026 10:20:30 +0200 Subject: [PATCH] =?UTF-8?q?Auto-Vertragsstatus:=20Lieferbest=C3=A4tigung?= =?UTF-8?q?=20hochladen=20=E2=86=92=20DRAFT=20auf=20ACTIVE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ergänzung zum Cancellation-Trigger: wenn ein ContractDocument mit documentType "Lieferbestätigung" hochgeladen wird und der Vertrag aktuell DRAFT ist, wird er automatisch auf ACTIVE gesetzt (+ Audit-Log). Greift an beiden Upload-Pfaden: - POST /api/contracts/:id/documents (Direkt-Upload via ContractDetail) - POST /api/emails/:id/attachments/:filename/save-as-contract-document (Email-Anhang als Vertragsdokument speichern) Vergleich case-insensitive + getrimmt auf "lieferbestätigung". Andere Typen (Auftragsformular etc.) lösen keinen Wechsel aus. Nicht-DRAFT- Verträge (ACTIVE/CANCELLED/EXPIRED/DEACTIVATED) bleiben unverändert. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/controllers/cachedEmail.controller.ts | 4 ++ .../src/controllers/contract.controller.ts | 4 ++ .../contractStatusScheduler.service.ts | 41 ++++++++++++++++++- backend/todo.md | 4 ++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index c393b3cb..07fe188f 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -11,6 +11,7 @@ import { decrypt } from '../utils/encryption.js'; import { ApiResponse } from '../types/index.js'; import { getCustomerTargets, getContractTargets, getIdentityDocumentTargets, getBankCardTargets, documentTargets } from '../config/documentTargets.config.js'; import { generateEmailPdf } from '../services/pdfService.js'; +import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusScheduler.service.js'; import { DocumentType } from '@prisma/client'; import prisma from '../lib/prisma.js'; import path from 'path'; @@ -2001,6 +2002,9 @@ export async function saveAttachmentAsContractDocument(req: Request, res: Respon }, }); + // Falls Lieferbestätigung + Vertrag DRAFT → automatisch auf ACTIVE + await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req); + res.json({ success: true, data: doc } as ApiResponse); } catch (error) { console.error('saveAttachmentAsContractDocument error:', error); diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 64e205cc..5c23a0e8 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -7,6 +7,7 @@ import * as authorizationService from '../services/authorization.service.js'; import { ApiResponse, AuthRequest } from '../types/index.js'; import { logChange } from '../services/audit.service.js'; import { canAccessContract } from '../utils/accessControl.js'; +import { maybeActivateOnDeliveryConfirmation } from '../services/contractStatusScheduler.service.js'; export async function getContracts(req: AuthRequest, res: Response): Promise { try { @@ -494,6 +495,9 @@ export async function uploadContractDocument(req: AuthRequest, res: Response): P customerId: contract?.customerId, }); + // Falls Lieferbestätigung + Vertrag DRAFT → automatisch auf ACTIVE + await maybeActivateOnDeliveryConfirmation(contractId, documentType, req); + res.status(201).json({ success: true, data: doc } as ApiResponse); } catch (error) { res.status(400).json({ diff --git a/backend/src/services/contractStatusScheduler.service.ts b/backend/src/services/contractStatusScheduler.service.ts index 1b8082ce..60b6a288 100644 --- a/backend/src/services/contractStatusScheduler.service.ts +++ b/backend/src/services/contractStatusScheduler.service.ts @@ -9,7 +9,7 @@ */ import cron from 'node-cron'; import prisma from '../lib/prisma.js'; -import { createAuditLog } from './audit.service.js'; +import { createAuditLog, logChange } from './audit.service.js'; async function runExpireCheck(): Promise { const today = new Date(); @@ -83,3 +83,42 @@ export function startContractStatusScheduler(): void { } export { runExpireCheck }; + +/** + * Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine + * Lieferbestätigung ist UND der Vertrag aktuell DRAFT ist, wird er auf + * ACTIVE gesetzt (+ Audit-Log). Andere Typen/Status bleiben unangetastet. + * + * Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown + * (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive + + * getrimmt zur Robustheit. + */ +export async function maybeActivateOnDeliveryConfirmation( + contractId: number, + documentType: string, + req: unknown, +): Promise { + if (!documentType || typeof documentType !== 'string') return; + if (documentType.trim().toLowerCase() !== 'lieferbestätigung') return; + + const contract = await prisma.contract.findUnique({ + where: { id: contractId }, + select: { status: true, contractNumber: true, customerId: true }, + }); + if (!contract || contract.status !== 'DRAFT') return; + + await prisma.contract.update({ + where: { id: contractId }, + data: { status: 'ACTIVE' }, + }); + + await logChange({ + req, + action: 'UPDATE', + resourceType: 'Contract', + resourceId: contractId.toString(), + label: `Vertrag ${contract.contractNumber} automatisch auf ACTIVE gesetzt (Lieferbestätigung hochgeladen)`, + details: { vorher: 'DRAFT', nachher: 'ACTIVE', trigger: 'Lieferbestätigung-Upload' }, + customerId: contract.customerId, + }); +} diff --git a/backend/todo.md b/backend/todo.md index 16c37a03..b1bfb362 100644 --- a/backend/todo.md +++ b/backend/todo.md @@ -104,6 +104,10 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung wenn Vertrag aktuell `ACTIVE` → auf `CANCELLED` setzen (Audit-Log). Der "Optionen"-Upload löst den Wechsel bewusst NICHT aus, da er für Vertragsänderungen (nicht echte Kündigungen) gedacht ist. + - Beim Upload einer `Lieferbestätigung` (ContractDocument via direkt-Upload + oder Email-Anhang-Import): wenn Vertrag aktuell `DRAFT` → auf `ACTIVE` + setzen (Audit-Log). Schreibweise stammt aus dem Frontend-Dropdown, + Vergleich case-insensitive + getrimmt. - Keine neuen Status eingeführt: `cancellationSentDate` vs. `cancellationConfirmationDate` genügen, um "gesendet vs. bestätigt" abzubilden. `ACTIVE` bleibt bis zur Bestätigung.