Files
opencrm/backend/src/services/contractHistory.service.ts
T
duffyduck 77602bb4ac contracts: VVL (Vertragsverlängerung) als Split-Button neben Folgevertrag
VVL = Vertragsverlängerung beim selben Anbieter (vs. Folgevertrag = i.d.R.
Anbieterwechsel).

Im Gegensatz zu createFollowUpContract wird ALLES kopiert:
- Provider, Tarif, Portal-Username/Passwort (verschlüsselt)
- Preise (basePrice/unitPrice/bonus etc.)
- Notes, Commission, Internet-Zugangsdaten, SIP-Daten, SIM-PINs
- ContractDocuments (1:1, gleiche Datei-Referenz)
- Detail-Tabellen (Energy/Internet/Mobile/TV/CarInsurance) komplett

Berechnet:
- newStartDate = oldStartDate + Vertragslaufzeit (Monate aus
  ContractDuration.code/description geparsed: "24M" / "24 Monate" / "2J")
- newEndDate = newStartDate + Laufzeit
- status = DRAFT (User bestätigt manuell)

NICHT kopiert:
- documentType "Auftragsformular" (das wird neu unterschrieben)
- cancellation*-Felder (alter Cancel-Flow nicht relevant)

Frontend:
- Split-Button: Hauptaktion "Folgevertrag anlegen" + ChevronDown-Pfeil
- Dropdown: "VVL anlegen" mit Bestätigungs-Modal
- Modal zeigt Vorhersage des neuen Startdatums (alter Start +
  Vertragslaufzeit als Hinweis)

History-Einträge wie bei Folgevertrag, mit eigenem VVL-Wording.
Doppel-Schutz: maximal 1 Folge-/VVL-Vertrag pro Vorgänger.

Live-verifiziert:
- Contract #17 (FIBER, 2026-05-01, 24M) → VVL mit Start 2028-05-01 ✓
- Provider/Tarif/Preise/Credentials 1:1 übernommen
- 2 Dokumente kopiert (außer Auftragsformular)
- History-Einträge in beiden Verträgen vorhanden

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:12:39 +02:00

164 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import prisma from '../lib/prisma.js';
export interface CreateHistoryEntryData {
title: string;
description?: string;
isAutomatic?: boolean;
createdBy: string;
}
/**
* Alle Historie-Einträge für einen Vertrag abrufen
*/
export async function getHistoryEntries(contractId: number) {
return prisma.contractHistoryEntry.findMany({
where: { contractId },
orderBy: { createdAt: 'desc' },
});
}
/**
* Einzelnen Historie-Eintrag abrufen
*/
export async function getHistoryEntry(contractId: number, entryId: number) {
return prisma.contractHistoryEntry.findFirst({
where: { id: entryId, contractId },
});
}
/**
* Neuen Historie-Eintrag erstellen
*/
export async function createHistoryEntry(contractId: number, data: CreateHistoryEntryData) {
// Prüfen ob Vertrag existiert
const contract = await prisma.contract.findUnique({
where: { id: contractId },
});
if (!contract) {
throw new Error('Vertrag nicht gefunden');
}
return prisma.contractHistoryEntry.create({
data: {
contractId,
title: data.title,
description: data.description,
isAutomatic: data.isAutomatic ?? false,
createdBy: data.createdBy,
},
});
}
/**
* Historie-Eintrag aktualisieren (nur manuelle Einträge)
*/
export async function updateHistoryEntry(
contractId: number,
entryId: number,
data: { title?: string; description?: string }
) {
const entry = await prisma.contractHistoryEntry.findFirst({
where: { id: entryId, contractId },
});
if (!entry) {
throw new Error('Historie-Eintrag nicht gefunden');
}
if (entry.isAutomatic) {
throw new Error('Automatische Einträge können nicht bearbeitet werden');
}
return prisma.contractHistoryEntry.update({
where: { id: entryId },
data: {
title: data.title,
description: data.description,
},
});
}
/**
* Historie-Eintrag löschen (nur manuelle Einträge)
*/
export async function deleteHistoryEntry(contractId: number, entryId: number) {
const entry = await prisma.contractHistoryEntry.findFirst({
where: { id: entryId, contractId },
});
if (!entry) {
throw new Error('Historie-Eintrag nicht gefunden');
}
if (entry.isAutomatic) {
throw new Error('Automatische Einträge können nicht gelöscht werden');
}
return prisma.contractHistoryEntry.delete({ where: { id: entryId } });
}
/**
* Automatischen Historie-Eintrag für Folgevertrag erstellen (im Vorgängervertrag)
*/
export async function createFollowUpHistoryEntry(
previousContractId: number,
newContractNumber: string,
createdBy: string
) {
return createHistoryEntry(previousContractId, {
title: `Folgevertrag erstellt: ${newContractNumber}`,
description: `Ein neuer Folgevertrag (${newContractNumber}) wurde aus diesem Vertrag erstellt.`,
isAutomatic: true,
createdBy,
});
}
/**
* Automatischen Historie-Eintrag für neuen Folgevertrag erstellen (im neuen Vertrag selbst)
*/
export async function createNewContractFromPredecessorEntry(
newContractId: number,
previousContractNumber: string,
createdBy: string
) {
return createHistoryEntry(newContractId, {
title: `Folgevertrag zu ${previousContractNumber}`,
description: `Dieser Vertrag wurde als Folgevertrag zu ${previousContractNumber} erstellt.`,
isAutomatic: true,
createdBy,
});
}
/**
* Automatischen Historie-Eintrag für VVL (Vertragsverlängerung) im Vorgängervertrag.
*/
export async function createRenewalHistoryEntry(
previousContractId: number,
newContractNumber: string,
createdBy: string
) {
return createHistoryEntry(previousContractId, {
title: `Vertragsverlängerung erstellt: ${newContractNumber}`,
description: `Eine Vertragsverlängerung (VVL) als ${newContractNumber} wurde aus diesem Vertrag erstellt alle Daten wurden 1:1 übernommen, das Auftragsdokument muss neu hochgeladen werden.`,
isAutomatic: true,
createdBy,
});
}
/**
* Automatischen Historie-Eintrag im neuen VVL-Vertrag.
*/
export async function createNewRenewalFromPredecessorEntry(
newContractId: number,
previousContractNumber: string,
createdBy: string
) {
return createHistoryEntry(newContractId, {
title: `VVL zu ${previousContractNumber}`,
description: `Dieser Vertrag wurde als Vertragsverlängerung (VVL) zu ${previousContractNumber} erstellt.`,
isAutomatic: true,
createdBy,
});
}