opencrm/backend/src/services/contract.service.ts

924 lines
30 KiB
TypeScript

import { ContractType, ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
import { encrypt, decrypt } from '../utils/encryption.js';
export interface ContractFilters {
customerId?: number;
customerIds?: number[]; // Für Kundenportal: eigene ID + vertretene Kunden
type?: ContractType;
status?: ContractStatus;
search?: string;
page?: number;
limit?: number;
}
export async function getAllContracts(filters: ContractFilters) {
const { customerId, customerIds, type, status, search, page = 1, limit = 20 } = filters;
const { skip, take } = paginate(page, limit);
const where: Record<string, unknown> = {};
// Entweder einzelne customerId ODER Liste von customerIds (für Kundenportal)
if (customerIds && customerIds.length > 0) {
where.customerId = { in: customerIds };
} else if (customerId) {
where.customerId = customerId;
}
if (type) where.type = type;
// Status-Filter: Deaktivierte Verträge standardmäßig ausblenden
if (status) {
where.status = status;
} else {
// Wenn kein Status-Filter gesetzt, alle außer DEACTIVATED anzeigen
where.status = { not: ContractStatus.DEACTIVATED };
}
if (search) {
where.OR = [
// Basis-Vertragsfelder
{ contractNumber: { contains: search } },
{ providerName: { contains: search } },
{ tariffName: { contains: search } },
{ customerNumberAtProvider: { contains: search } },
{ provider: { name: { contains: search } } },
{ tariff: { name: { contains: search } } },
// Kundenname
{ customer: { firstName: { contains: search } } },
{ customer: { lastName: { contains: search } } },
{ customer: { companyName: { contains: search } } },
{ customer: { customerNumber: { contains: search } } },
// Internet-Vertragsdetails
{ internetDetails: { routerSerialNumber: { contains: search } } },
{ internetDetails: { homeId: { contains: search } } },
{ internetDetails: { activationCode: { contains: search } } },
{ internetDetails: { phoneNumbers: { some: { phoneNumber: { contains: search } } } } },
// Mobilfunk-Vertragsdetails
{ mobileDetails: { phoneNumber: { contains: search } } },
{ mobileDetails: { simCardNumber: { contains: search } } },
{ mobileDetails: { deviceImei: { contains: search } } },
{ mobileDetails: { simCards: { some: { phoneNumber: { contains: search } } } } },
{ mobileDetails: { simCards: { some: { simCardNumber: { contains: search } } } } },
// Energie-Vertragsdetails (Zählernummer)
{ energyDetails: { meter: { meterNumber: { contains: search } } } },
// TV-Vertragsdetails
{ tvDetails: { smartcardNumber: { contains: search } } },
// KFZ-Versicherung
{ carInsuranceDetails: { licensePlate: { contains: search } } },
{ carInsuranceDetails: { vin: { contains: search } } },
{ carInsuranceDetails: { policyNumber: { contains: search } } },
];
}
const [contracts, total] = await Promise.all([
prisma.contract.findMany({
where,
skip,
take,
orderBy: [{ createdAt: 'desc' }],
include: {
customer: {
select: {
id: true,
customerNumber: true,
firstName: true,
lastName: true,
companyName: true,
},
},
address: true,
billingAddress: true,
salesPlatform: true,
cancellationPeriod: true,
contractDuration: true,
provider: true,
tariff: true,
contractCategory: true,
},
}),
prisma.contract.count({ where }),
]);
return {
contracts,
pagination: buildPaginationResponse(page, limit, total),
};
}
export async function getContractById(id: number, decryptPassword = false) {
const contract = await prisma.contract.findUnique({
where: { id },
include: {
customer: true,
address: true,
billingAddress: true,
bankCard: true,
identityDocument: true,
salesPlatform: true,
cancellationPeriod: true,
contractDuration: true,
provider: true,
tariff: true,
contractCategory: true,
previousProvider: true,
previousContract: {
include: {
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
},
},
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
stressfreiEmail: true,
invoices: { orderBy: { invoiceDate: 'desc' as const } },
documents: { orderBy: { createdAt: 'desc' as const } },
followUpContract: {
select: { id: true, contractNumber: true, status: true },
},
},
});
if (!contract) return null;
// Decrypt password if requested and exists
if (decryptPassword && contract.portalPasswordEncrypted) {
try {
(contract as Record<string, unknown>).portalPasswordDecrypted = decrypt(
contract.portalPasswordEncrypted
);
} catch {
// Password decryption failed, leave as is
}
}
return contract;
}
interface ContractCreateData {
customerId: number;
type: ContractType;
contractCategoryId?: number;
status?: ContractStatus;
addressId?: number;
billingAddressId?: number;
bankCardId?: number;
identityDocumentId?: number;
salesPlatformId?: number;
previousContractId?: number;
providerId?: number;
tariffId?: number;
providerName?: string;
tariffName?: string;
customerNumberAtProvider?: string;
priceFirst12Months?: string;
priceFrom13Months?: string;
priceAfter24Months?: string;
startDate?: Date;
endDate?: Date;
cancellationPeriodId?: number;
contractDurationId?: number;
commission?: number;
portalUsername?: string;
portalPassword?: string;
stressfreiEmailId?: number;
notes?: string;
// Kündigungsdaten
cancellationConfirmationDate?: Date;
cancellationConfirmationOptionsDate?: Date;
wasSpecialCancellation?: boolean;
// Type-specific details
energyDetails?: {
meterId?: number;
annualConsumption?: number;
basePrice?: number;
unitPrice?: number;
bonus?: number;
previousProviderName?: string;
previousCustomerNumber?: string;
};
internetDetails?: {
downloadSpeed?: number;
uploadSpeed?: number;
routerModel?: string;
routerSerialNumber?: string;
installationDate?: Date;
// Internet-Zugangsdaten
internetUsername?: string;
internetPassword?: string;
// Objekt & Lage
propertyType?: string;
propertyLocation?: string;
connectionLocation?: string;
// Glasfaser-spezifisch
homeId?: string;
// Vodafone DSL/Kabel spezifisch
activationCode?: string;
phoneNumbers?: {
id?: number;
phoneNumber: string;
isMain?: boolean;
sipUsername?: string;
sipPassword?: string;
sipServer?: string;
}[];
};
mobileDetails?: {
requiresMultisim?: boolean;
dataVolume?: number;
includedMinutes?: number;
includedSMS?: number;
deviceModel?: string;
deviceImei?: string;
// Legacy-Felder
phoneNumber?: string;
simCardNumber?: string;
// SIM-Karten Liste
simCards?: {
id?: number;
phoneNumber?: string;
simCardNumber?: string;
pin?: string;
puk?: string;
isMultisim?: boolean;
isMain?: boolean;
}[];
};
tvDetails?: {
receiverModel?: string;
smartcardNumber?: string;
package?: string;
};
carInsuranceDetails?: {
licensePlate?: string;
hsn?: string;
tsn?: string;
vin?: string;
vehicleType?: string;
firstRegistration?: Date;
noClaimsClass?: string;
insuranceType?: 'LIABILITY' | 'PARTIAL' | 'FULL';
deductiblePartial?: number;
deductibleFull?: number;
policyNumber?: string;
previousInsurer?: string;
};
}
export async function createContract(data: ContractCreateData) {
const {
energyDetails,
internetDetails,
mobileDetails,
tvDetails,
carInsuranceDetails,
portalPassword,
...contractData
} = data;
// Encrypt password if provided
const portalPasswordEncrypted = portalPassword
? encrypt(portalPassword)
: undefined;
const contract = await prisma.contract.create({
data: {
...contractData,
contractNumber: generateContractNumber(data.type),
portalPasswordEncrypted,
...(energyDetails && ['ELECTRICITY', 'GAS'].includes(data.type)
? { energyDetails: { create: energyDetails } }
: {}),
...(internetDetails && ['DSL', 'CABLE', 'FIBER'].includes(data.type)
? {
internetDetails: {
create: {
downloadSpeed: internetDetails.downloadSpeed,
uploadSpeed: internetDetails.uploadSpeed,
routerModel: internetDetails.routerModel,
routerSerialNumber: internetDetails.routerSerialNumber,
installationDate: internetDetails.installationDate,
internetUsername: internetDetails.internetUsername,
internetPasswordEncrypted: internetDetails.internetPassword
? encrypt(internetDetails.internetPassword)
: undefined,
propertyType: internetDetails.propertyType,
propertyLocation: internetDetails.propertyLocation,
connectionLocation: internetDetails.connectionLocation,
homeId: internetDetails.homeId,
activationCode: internetDetails.activationCode,
phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0
? {
create: internetDetails.phoneNumbers.map((pn) => ({
phoneNumber: pn.phoneNumber,
isMain: pn.isMain ?? false,
sipUsername: pn.sipUsername,
sipPasswordEncrypted: pn.sipPassword
? encrypt(pn.sipPassword)
: undefined,
sipServer: pn.sipServer,
})),
}
: undefined,
},
},
}
: {}),
...(mobileDetails && data.type === 'MOBILE'
? {
mobileDetails: {
create: {
requiresMultisim: mobileDetails.requiresMultisim,
dataVolume: mobileDetails.dataVolume,
includedMinutes: mobileDetails.includedMinutes,
includedSMS: mobileDetails.includedSMS,
deviceModel: mobileDetails.deviceModel,
deviceImei: mobileDetails.deviceImei,
phoneNumber: mobileDetails.phoneNumber,
simCardNumber: mobileDetails.simCardNumber,
simCards: mobileDetails.simCards
? {
create: mobileDetails.simCards.map((sc) => ({
phoneNumber: sc.phoneNumber,
simCardNumber: sc.simCardNumber,
pin: sc.pin ? encrypt(sc.pin) : undefined,
puk: sc.puk ? encrypt(sc.puk) : undefined,
isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false,
})),
}
: undefined,
},
},
}
: {}),
...(tvDetails && data.type === 'TV'
? { tvDetails: { create: tvDetails } }
: {}),
...(carInsuranceDetails && data.type === 'CAR_INSURANCE'
? { carInsuranceDetails: { create: carInsuranceDetails } }
: {}),
},
include: {
customer: true,
address: true,
billingAddress: true,
salesPlatform: true,
energyDetails: true,
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
},
});
return contract;
}
export async function updateContract(
id: number,
data: Partial<ContractCreateData>
) {
const {
energyDetails,
internetDetails,
mobileDetails,
tvDetails,
carInsuranceDetails,
portalPassword,
...contractData
} = data;
// Encrypt password if provided
const portalPasswordEncrypted = portalPassword
? encrypt(portalPassword)
: undefined;
// Update main contract
await prisma.contract.update({
where: { id },
data: {
...contractData,
...(portalPasswordEncrypted ? { portalPasswordEncrypted } : {}),
},
});
// Update type-specific details
if (energyDetails) {
const existingEcd = await prisma.energyContractDetails.findUnique({
where: { contractId: id },
select: { id: true, meterId: true },
});
await prisma.energyContractDetails.upsert({
where: { contractId: id },
update: energyDetails,
create: { contractId: id, ...energyDetails },
});
// ContractMeter synchronisieren wenn sich der Zähler ändert
if (energyDetails.meterId !== undefined && existingEcd) {
const oldMeterId = existingEcd.meterId;
const newMeterId = energyDetails.meterId;
if (oldMeterId !== newMeterId) {
// Alle alten ContractMeter-Einträge entfernen
await prisma.contractMeter.deleteMany({
where: { energyContractDetailsId: existingEcd.id },
});
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
if (newMeterId) {
const contract = await prisma.contract.findUnique({
where: { id },
select: { startDate: true },
});
await prisma.contractMeter.create({
data: {
energyContractDetailsId: existingEcd.id,
meterId: newMeterId,
position: 0,
installedAt: contract?.startDate,
},
});
}
}
}
}
if (internetDetails) {
const { phoneNumbers, internetPassword, ...internetData } = internetDetails;
const existing = await prisma.internetContractDetails.findUnique({
where: { contractId: id },
include: { phoneNumbers: true },
});
// Prepare internet data with encryption
const preparedInternetData = {
downloadSpeed: internetData.downloadSpeed,
uploadSpeed: internetData.uploadSpeed,
routerModel: internetData.routerModel,
routerSerialNumber: internetData.routerSerialNumber,
installationDate: internetData.installationDate,
internetUsername: internetData.internetUsername,
// Only update password if new value provided, otherwise keep existing
...(internetPassword
? { internetPasswordEncrypted: encrypt(internetPassword) }
: {}),
propertyType: internetData.propertyType,
propertyLocation: internetData.propertyLocation,
connectionLocation: internetData.connectionLocation,
homeId: internetData.homeId,
activationCode: internetData.activationCode,
};
if (existing) {
await prisma.internetContractDetails.update({
where: { contractId: id },
data: preparedInternetData,
});
if (phoneNumbers) {
// Get existing phone numbers for preserving encrypted passwords
const existingPhoneNumbers = existing.phoneNumbers || [];
// Delete all existing phone numbers
await prisma.phoneNumber.deleteMany({
where: { internetContractDetailsId: existing.id },
});
// Create new phone numbers with encryption
await prisma.phoneNumber.createMany({
data: phoneNumbers.map((pn) => {
// Find existing entry to preserve sipPassword if not changed
const existingPn = pn.id
? existingPhoneNumbers.find((e) => e.id === pn.id)
: undefined;
return {
internetContractDetailsId: existing.id,
phoneNumber: pn.phoneNumber,
isMain: pn.isMain ?? false,
sipUsername: pn.sipUsername,
// Preserve existing sipPassword if no new value provided
sipPasswordEncrypted: pn.sipPassword
? encrypt(pn.sipPassword)
: existingPn?.sipPasswordEncrypted ?? undefined,
sipServer: pn.sipServer,
};
}),
});
}
} else {
await prisma.internetContractDetails.create({
data: {
contractId: id,
...preparedInternetData,
...(internetPassword
? { internetPasswordEncrypted: encrypt(internetPassword) }
: {}),
phoneNumbers: phoneNumbers
? {
create: phoneNumbers.map((pn) => ({
phoneNumber: pn.phoneNumber,
isMain: pn.isMain ?? false,
sipUsername: pn.sipUsername,
sipPasswordEncrypted: pn.sipPassword
? encrypt(pn.sipPassword)
: undefined,
sipServer: pn.sipServer,
})),
}
: undefined,
},
});
}
}
if (mobileDetails) {
const { simCards, ...mobileData } = mobileDetails;
const existing = await prisma.mobileContractDetails.findUnique({
where: { contractId: id },
});
if (existing) {
await prisma.mobileContractDetails.update({
where: { contractId: id },
data: mobileData,
});
if (simCards) {
// Get existing sim cards to preserve PIN/PUK if not provided
const existingSimCards = await prisma.simCard.findMany({
where: { mobileDetailsId: existing.id },
});
const existingSimCardMap = new Map(existingSimCards.map(sc => [sc.id, sc]));
// Delete existing sim cards
await prisma.simCard.deleteMany({
where: { mobileDetailsId: existing.id },
});
// Create new sim cards, preserving PIN/PUK if not provided
await prisma.simCard.createMany({
data: simCards.map((sc) => {
const existingSc = sc.id ? existingSimCardMap.get(sc.id) : undefined;
return {
mobileDetailsId: existing.id,
phoneNumber: sc.phoneNumber,
simCardNumber: sc.simCardNumber,
// Preserve existing PIN/PUK if no new value provided
pin: sc.pin ? encrypt(sc.pin) : (existingSc?.pin ?? undefined),
puk: sc.puk ? encrypt(sc.puk) : (existingSc?.puk ?? undefined),
isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false,
};
}),
});
}
} else {
await prisma.mobileContractDetails.create({
data: {
contractId: id,
...mobileData,
simCards: simCards
? {
create: simCards.map((sc) => ({
phoneNumber: sc.phoneNumber,
simCardNumber: sc.simCardNumber,
pin: sc.pin ? encrypt(sc.pin) : undefined,
puk: sc.puk ? encrypt(sc.puk) : undefined,
isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false,
})),
}
: undefined,
},
});
}
}
if (tvDetails) {
await prisma.tvContractDetails.upsert({
where: { contractId: id },
update: tvDetails,
create: { contractId: id, ...tvDetails },
});
}
if (carInsuranceDetails) {
await prisma.carInsuranceDetails.upsert({
where: { contractId: id },
update: carInsuranceDetails,
create: { contractId: id, ...carInsuranceDetails },
});
}
return getContractById(id);
}
export async function deleteContract(id: number) {
// Vertragskette erhalten beim Löschen:
// Wenn A → B → C und B gelöscht wird, soll C direkt auf A zeigen (A → C)
// 1. Zu löschenden Vertrag holen um dessen Vorgänger zu kennen
const contractToDelete = await prisma.contract.findUnique({
where: { id },
select: { previousContractId: true },
});
// 2. Folgevertrag(e) mit dem Vorgänger des gelöschten Vertrags verbinden
// So bleibt die Kette erhalten: A → B → C wird zu A → C
await prisma.contract.updateMany({
where: { previousContractId: id },
data: { previousContractId: contractToDelete?.previousContractId ?? null },
});
return prisma.contract.delete({ where: { id } });
}
export async function createFollowUpContract(previousContractId: number) {
const previousContract = await getContractById(previousContractId);
if (!previousContract) {
throw new Error('Vorgängervertrag nicht gefunden');
}
// Prüfen ob bereits ein Folgevertrag existiert
const existingFollowUp = await prisma.contract.findFirst({
where: { previousContractId },
select: { id: true, contractNumber: true },
});
if (existingFollowUp) {
throw new Error(`Es existiert bereits ein Folgevertrag: ${existingFollowUp.contractNumber}`);
}
// Copy data but exclude provider credentials and some fields
const newContractData: ContractCreateData = {
customerId: previousContract.customerId,
type: previousContract.type,
status: 'DRAFT',
addressId: previousContract.addressId ?? undefined,
bankCardId: previousContract.bankCardId ?? undefined,
identityDocumentId: previousContract.identityDocumentId ?? undefined,
salesPlatformId: previousContract.salesPlatformId ?? undefined,
previousContractId: previousContract.id,
// Explicitly NOT copying: providerName, tariffName, portalUsername, portalPassword, price fields
cancellationPeriodId: previousContract.cancellationPeriodId ?? undefined,
contractDurationId: previousContract.contractDurationId ?? undefined,
// notes nicht mehr automatisch setzen - wird jetzt über Historie-Eintrag dokumentiert
};
// Copy type-specific details (without credentials)
if (previousContract.energyDetails) {
newContractData.energyDetails = {
meterId: previousContract.energyDetails.meterId ?? undefined,
annualConsumption:
previousContract.energyDetails.annualConsumption ?? undefined,
basePrice: previousContract.energyDetails.basePrice ?? undefined,
unitPrice: previousContract.energyDetails.unitPrice ?? undefined,
bonus: previousContract.energyDetails.bonus ?? undefined,
previousProviderName: previousContract.providerName ?? undefined,
previousCustomerNumber:
previousContract.customerNumberAtProvider ?? undefined,
};
}
if (previousContract.internetDetails) {
newContractData.internetDetails = {
downloadSpeed:
previousContract.internetDetails.downloadSpeed ?? undefined,
uploadSpeed: previousContract.internetDetails.uploadSpeed ?? undefined,
routerModel: previousContract.internetDetails.routerModel ?? undefined,
routerSerialNumber:
previousContract.internetDetails.routerSerialNumber ?? undefined,
phoneNumbers: previousContract.internetDetails.phoneNumbers.map((pn) => ({
phoneNumber: pn.phoneNumber,
isMain: pn.isMain,
})),
};
}
if (previousContract.mobileDetails) {
newContractData.mobileDetails = {
requiresMultisim: previousContract.mobileDetails.requiresMultisim ?? undefined,
dataVolume: previousContract.mobileDetails.dataVolume ?? undefined,
includedMinutes:
previousContract.mobileDetails.includedMinutes ?? undefined,
includedSMS: previousContract.mobileDetails.includedSMS ?? undefined,
deviceModel: previousContract.mobileDetails.deviceModel ?? undefined,
deviceImei: previousContract.mobileDetails.deviceImei ?? undefined,
phoneNumber: previousContract.mobileDetails.phoneNumber ?? undefined,
simCardNumber: previousContract.mobileDetails.simCardNumber ?? undefined,
// Copy simCards without PIN/PUK (security)
simCards: previousContract.mobileDetails.simCards?.map((sc) => ({
phoneNumber: sc.phoneNumber ?? undefined,
simCardNumber: sc.simCardNumber ?? undefined,
isMultisim: sc.isMultisim,
isMain: sc.isMain,
})),
};
}
if (previousContract.tvDetails) {
newContractData.tvDetails = {
receiverModel: previousContract.tvDetails.receiverModel ?? undefined,
smartcardNumber:
previousContract.tvDetails.smartcardNumber ?? undefined,
package: previousContract.tvDetails.package ?? undefined,
};
}
if (previousContract.carInsuranceDetails) {
newContractData.carInsuranceDetails = {
licensePlate:
previousContract.carInsuranceDetails.licensePlate ?? undefined,
hsn: previousContract.carInsuranceDetails.hsn ?? undefined,
tsn: previousContract.carInsuranceDetails.tsn ?? undefined,
vin: previousContract.carInsuranceDetails.vin ?? undefined,
vehicleType: previousContract.carInsuranceDetails.vehicleType ?? undefined,
firstRegistration:
previousContract.carInsuranceDetails.firstRegistration ?? undefined,
noClaimsClass:
previousContract.carInsuranceDetails.noClaimsClass ?? undefined,
insuranceType: previousContract.carInsuranceDetails.insuranceType,
deductiblePartial:
previousContract.carInsuranceDetails.deductiblePartial ?? undefined,
deductibleFull:
previousContract.carInsuranceDetails.deductibleFull ?? undefined,
previousInsurer: previousContract.providerName ?? undefined,
};
}
return createContract(newContractData);
}
// Decrypt password for viewing
export async function getContractPassword(id: number): Promise<string | null> {
const contract = await prisma.contract.findUnique({
where: { id },
select: { portalPasswordEncrypted: true },
});
if (!contract?.portalPasswordEncrypted) return null;
try {
return decrypt(contract.portalPasswordEncrypted);
} catch {
return null;
}
}
// Decrypt SimCard PIN/PUK
export async function getSimCardCredentials(simCardId: number): Promise<{ pin: string | null; puk: string | null }> {
const simCard = await prisma.simCard.findUnique({
where: { id: simCardId },
select: { pin: true, puk: true },
});
if (!simCard) return { pin: null, puk: null };
try {
return {
pin: simCard.pin ? decrypt(simCard.pin) : null,
puk: simCard.puk ? decrypt(simCard.puk) : null,
};
} catch {
return { pin: null, puk: null };
}
}
// Decrypt Internet password
export async function getInternetCredentials(contractId: number): Promise<{ password: string | null }> {
const internetDetails = await prisma.internetContractDetails.findUnique({
where: { contractId },
select: { internetPasswordEncrypted: true },
});
if (!internetDetails?.internetPasswordEncrypted) return { password: null };
try {
return {
password: decrypt(internetDetails.internetPasswordEncrypted),
};
} catch {
return { password: null };
}
}
// Decrypt SIP password for a phone number
export async function getSipCredentials(phoneNumberId: number): Promise<{ password: string | null }> {
const phoneNumber = await prisma.phoneNumber.findUnique({
where: { id: phoneNumberId },
select: { sipPasswordEncrypted: true },
});
if (!phoneNumber?.sipPasswordEncrypted) return { password: null };
try {
return {
password: decrypt(phoneNumber.sipPasswordEncrypted),
};
} catch {
return { password: null };
}
}
// ==================== VERTRAGSBAUM FÜR KUNDENANSICHT ====================
export interface ContractTreeNode {
contract: {
id: number;
contractNumber: string;
type: ContractType;
status: ContractStatus;
startDate: Date | null;
endDate: Date | null;
providerName: string | null;
tariffName: string | null;
previousContractId: number | null;
provider?: { id: number; name: string } | null;
tariff?: { id: number; name: string } | null;
contractCategory?: { id: number; name: string } | null;
};
predecessors: ContractTreeNode[];
hasHistory: boolean;
}
/**
* Verträge eines Kunden als Baumstruktur abrufen.
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
* Vorgänger werden rekursiv eingebettet.
*/
export async function getContractTreeForCustomer(customerId: number): Promise<ContractTreeNode[]> {
// Alle Verträge des Kunden laden (außer DEACTIVATED)
const allContracts = await prisma.contract.findMany({
where: {
customerId,
status: { not: ContractStatus.DEACTIVATED },
},
select: {
id: true,
contractNumber: true,
type: true,
status: true,
startDate: true,
endDate: true,
providerName: true,
tariffName: true,
previousContractId: true,
provider: { select: { id: true, name: true } },
tariff: { select: { id: true, name: true } },
contractCategory: { select: { id: true, name: true } },
},
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
});
// Map für schnellen Zugriff: contractId -> contract
const contractMap = new Map(allContracts.map(c => [c.id, c]));
// Set der IDs die als Vorgänger referenziert werden
const predecessorIds = new Set(
allContracts
.filter(c => c.previousContractId !== null)
.map(c => c.previousContractId!)
);
// Wurzelverträge = Verträge die keinen Nachfolger haben
// (werden von keinem anderen Vertrag als previousContractId referenziert)
const rootContracts = allContracts.filter(c => !predecessorIds.has(c.id));
// Rekursive Funktion um Vorgängerkette aufzubauen
function buildPredecessorChain(contractId: number | null): ContractTreeNode[] {
if (contractId === null) return [];
const contract = contractMap.get(contractId);
if (!contract) return [];
const predecessors = buildPredecessorChain(contract.previousContractId);
return [{
contract,
predecessors,
hasHistory: predecessors.length > 0,
}];
}
// Baumstruktur für jeden Wurzelvertrag aufbauen
const tree: ContractTreeNode[] = rootContracts.map(contract => {
const predecessors = buildPredecessorChain(contract.previousContractId);
return {
contract,
predecessors,
hasHistory: predecessors.length > 0,
};
});
return tree;
}