added invoices and status in cockpit, created info button for contract status types

This commit is contained in:
2026-02-08 01:18:12 +01:00
parent 1ad4fe0819
commit aee48a8ccb
45 changed files with 4543 additions and 863 deletions
@@ -3,6 +3,7 @@
import { Request, Response } from 'express';
import * as cachedEmailService from '../services/cachedEmail.service.js';
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
import * as invoiceService from '../services/invoice.service.js';
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js';
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
@@ -885,6 +886,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
contract?: {
id: number;
contractNumber: string;
type: string;
energyDetailsId?: number;
slots: TargetSlot[];
};
}
@@ -954,10 +957,14 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
select: {
id: true,
contractNumber: true,
type: true,
cancellationLetterPath: true,
cancellationConfirmationPath: true,
cancellationLetterOptionsPath: true,
cancellationConfirmationOptionsPath: true,
energyDetails: {
select: { id: true },
},
},
});
@@ -966,6 +973,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
response.contract = {
id: contract.id,
contractNumber: contract.contractNumber,
type: contract.type,
energyDetailsId: contract.energyDetails?.id,
slots: contractTargets.map(target => ({
key: target.key,
label: target.label,
@@ -1518,3 +1527,309 @@ export async function saveEmailAsPdf(req: Request, res: Response): Promise<void>
} as ApiResponse);
}
}
// ==================== SAVE EMAIL AS INVOICE ====================
// E-Mail als PDF exportieren und als Rechnung speichern
export async function saveEmailAsInvoice(req: Request, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const { invoiceDate, invoiceType, notes } = req.body;
console.log('[saveEmailAsInvoice] Request:', { emailId, invoiceDate, invoiceType, notes });
// Validierung
if (!invoiceDate || !invoiceType) {
res.status(400).json({
success: false,
error: 'invoiceDate und invoiceType sind erforderlich',
} as ApiResponse);
return;
}
// Validiere invoiceType
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
res.status(400).json({
success: false,
error: 'Ungültiger Rechnungstyp',
} as ApiResponse);
return;
}
// E-Mail aus Cache laden
const email = await cachedEmailService.getCachedEmailById(emailId);
if (!email) {
res.status(404).json({
success: false,
error: 'E-Mail nicht gefunden',
} as ApiResponse);
return;
}
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
if (!email.contractId) {
res.status(400).json({
success: false,
error: 'E-Mail ist keinem Vertrag zugeordnet',
} as ApiResponse);
return;
}
// Vertrag laden und prüfen ob es ein Energievertrag ist
const contract = await prisma.contract.findUnique({
where: { id: email.contractId },
include: { energyDetails: true },
});
if (!contract) {
res.status(404).json({
success: false,
error: 'Vertrag nicht gefunden',
} as ApiResponse);
return;
}
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
res.status(400).json({
success: false,
error: 'Nur für Strom- und Gas-Verträge verfügbar',
} as ApiResponse);
return;
}
if (!contract.energyDetails) {
res.status(400).json({
success: false,
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
} as ApiResponse);
return;
}
// Empfänger-Adressen parsen (JSON Array)
let toAddresses: string[] = [];
let ccAddresses: string[] = [];
try {
toAddresses = JSON.parse(email.toAddresses);
} catch { toAddresses = [email.toAddresses]; }
try {
if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses);
} catch { /* ignore */ }
// PDF generieren
const pdfBuffer = await generateEmailPdf({
from: email.fromAddress,
to: toAddresses.join(', '),
cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
subject: email.subject || '(Kein Betreff)',
date: email.receivedAt,
bodyText: email.textBody || undefined,
bodyHtml: email.htmlBody || undefined,
});
// Uploads-Verzeichnis erstellen
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Eindeutigen Dateinamen generieren
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const newFilename = `invoice-email-${uniqueSuffix}.pdf`;
const filePath = path.join(uploadsDir, newFilename);
const relativePath = `/uploads/invoices/${newFilename}`;
// PDF speichern
fs.writeFileSync(filePath, pdfBuffer);
// Invoice in DB erstellen
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
invoiceDate: new Date(invoiceDate),
invoiceType,
documentPath: relativePath,
notes: notes || undefined,
});
res.json({
success: true,
data: invoice,
} as ApiResponse);
} catch (error) {
console.error('saveEmailAsInvoice error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(500).json({
success: false,
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
} as ApiResponse);
}
}
// ==================== SAVE ATTACHMENT AS INVOICE ====================
// E-Mail-Anhang als Rechnung speichern
export async function saveAttachmentAsInvoice(req: Request, res: Response): Promise<void> {
try {
const emailId = parseInt(req.params.id);
const filename = decodeURIComponent(req.params.filename);
const { invoiceDate, invoiceType, notes } = req.body;
console.log('[saveAttachmentAsInvoice] Request:', { emailId, filename, invoiceDate, invoiceType, notes });
// Validierung
if (!invoiceDate || !invoiceType) {
res.status(400).json({
success: false,
error: 'invoiceDate und invoiceType sind erforderlich',
} as ApiResponse);
return;
}
// Validiere invoiceType
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
res.status(400).json({
success: false,
error: 'Ungültiger Rechnungstyp',
} as ApiResponse);
return;
}
// E-Mail aus Cache laden
const email = await cachedEmailService.getCachedEmailById(emailId);
if (!email) {
res.status(404).json({
success: false,
error: 'E-Mail nicht gefunden',
} as ApiResponse);
return;
}
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
if (!email.contractId) {
res.status(400).json({
success: false,
error: 'E-Mail ist keinem Vertrag zugeordnet',
} as ApiResponse);
return;
}
// Vertrag laden und prüfen ob es ein Energievertrag ist
const contract = await prisma.contract.findUnique({
where: { id: email.contractId },
include: { energyDetails: true },
});
if (!contract) {
res.status(404).json({
success: false,
error: 'Vertrag nicht gefunden',
} as ApiResponse);
return;
}
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
res.status(400).json({
success: false,
error: 'Nur für Strom- und Gas-Verträge verfügbar',
} as ApiResponse);
return;
}
if (!contract.energyDetails) {
res.status(400).json({
success: false,
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
} as ApiResponse);
return;
}
// Für gesendete E-Mails: Prüfen ob UID vorhanden
if (email.folder === 'SENT' && email.uid === 0) {
res.status(400).json({
success: false,
error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
} as ApiResponse);
return;
}
// StressfreiEmail laden um Zugangsdaten zu bekommen
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
res.status(400).json({
success: false,
error: 'Keine Mailbox-Zugangsdaten verfügbar',
} as ApiResponse);
return;
}
// IMAP-Einstellungen laden
const settings = await getImapSmtpSettings();
if (!settings) {
res.status(400).json({
success: false,
error: 'Keine E-Mail-Provider-Einstellungen gefunden',
} as ApiResponse);
return;
}
// Passwort entschlüsseln
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
// IMAP-Credentials zusammenstellen
const credentials: ImapCredentials = {
host: settings.imapServer,
port: settings.imapPort,
user: stressfreiEmail.email,
password,
encryption: settings.imapEncryption,
allowSelfSignedCerts: settings.allowSelfSignedCerts,
};
// IMAP-Ordner bestimmen
const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
// Anhang vom IMAP-Server laden
const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
if (!attachment) {
res.status(404).json({
success: false,
error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
} as ApiResponse);
return;
}
// Uploads-Verzeichnis erstellen
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Dateiendung extrahieren
const ext = path.extname(filename) || '.pdf';
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const newFilename = `invoice-attachment-${uniqueSuffix}${ext}`;
const filePath = path.join(uploadsDir, newFilename);
const relativePath = `/uploads/invoices/${newFilename}`;
// Datei speichern
fs.writeFileSync(filePath, attachment.content);
// Invoice in DB erstellen
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
invoiceDate: new Date(invoiceDate),
invoiceType,
documentPath: relativePath,
notes: notes || undefined,
});
res.json({
success: true,
data: invoice,
} as ApiResponse);
} catch (error) {
console.error('saveAttachmentAsInvoice error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
res.status(500).json({
success: false,
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
} as ApiResponse);
}
}
@@ -0,0 +1,126 @@
import { Request, Response } from 'express';
import * as invoiceService from '../services/invoice.service.js';
import { ApiResponse } from '../types/index.js';
/**
* Alle Rechnungen für ein EnergyContractDetails abrufen
*/
export async function getInvoices(req: Request, res: Response): Promise<void> {
try {
const ecdId = parseInt(req.params.ecdId);
const invoices = await invoiceService.getInvoices(ecdId);
res.json({ success: true, data: invoices } as ApiResponse);
} catch (error) {
console.error('getInvoices error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Rechnungen',
} as ApiResponse);
}
}
/**
* Einzelne Rechnung abrufen
*/
export async function getInvoice(req: Request, res: Response): Promise<void> {
try {
const ecdId = parseInt(req.params.ecdId);
const invoiceId = parseInt(req.params.invoiceId);
const invoice = await invoiceService.getInvoice(ecdId, invoiceId);
if (!invoice) {
res.status(404).json({
success: false,
error: 'Rechnung nicht gefunden',
} as ApiResponse);
return;
}
res.json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
console.error('getInvoice error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Rechnung',
} as ApiResponse);
}
}
/**
* Neue Rechnung hinzufügen
*/
export async function addInvoice(req: Request, res: Response): Promise<void> {
try {
const ecdId = parseInt(req.params.ecdId);
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
if (!invoiceDate || !invoiceType) {
res.status(400).json({
success: false,
error: 'invoiceDate und invoiceType sind erforderlich',
} as ApiResponse);
return;
}
const invoice = await invoiceService.addInvoice(ecdId, {
invoiceDate: new Date(invoiceDate),
invoiceType,
documentPath,
notes,
});
res.status(201).json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
console.error('addInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen der Rechnung',
} as ApiResponse);
}
}
/**
* Rechnung aktualisieren
*/
export async function updateInvoice(req: Request, res: Response): Promise<void> {
try {
const ecdId = parseInt(req.params.ecdId);
const invoiceId = parseInt(req.params.invoiceId);
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
const invoice = await invoiceService.updateInvoice(ecdId, invoiceId, {
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
invoiceType,
documentPath,
notes,
});
res.json({ success: true, data: invoice } as ApiResponse);
} catch (error) {
console.error('updateInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rechnung',
} as ApiResponse);
}
}
/**
* Rechnung löschen
*/
export async function deleteInvoice(req: Request, res: Response): Promise<void> {
try {
const ecdId = parseInt(req.params.ecdId);
const invoiceId = parseInt(req.params.invoiceId);
await invoiceService.deleteInvoice(ecdId, invoiceId);
res.json({ success: true, data: null } as ApiResponse);
} catch (error) {
console.error('deleteInvoice error:', error);
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rechnung',
} as ApiResponse);
}
}
+2
View File
@@ -24,6 +24,7 @@ import contractTaskRoutes from './routes/contractTask.routes.js';
import appSettingRoutes from './routes/appSetting.routes.js';
import emailProviderRoutes from './routes/emailProvider.routes.js';
import cachedEmailRoutes from './routes/cachedEmail.routes.js';
import invoiceRoutes from './routes/invoice.routes.js';
dotenv.config();
@@ -59,6 +60,7 @@ app.use('/api', contractTaskRoutes);
app.use('/api/settings', appSettingRoutes);
app.use('/api/email-providers', emailProviderRoutes);
app.use('/api', cachedEmailRoutes);
app.use('/api/energy-details', invoiceRoutes);
// Health check
app.get('/api/health', (req, res) => {
+18
View File
@@ -185,6 +185,24 @@ router.post(
cachedEmailController.saveEmailAsPdf
);
// E-Mail als PDF exportieren und als Rechnung speichern
// POST /api/emails/:id/save-as-invoice { invoiceDate, invoiceType, notes? }
router.post(
'/emails/:id/save-as-invoice',
authenticate,
requirePermission('contracts:update'),
cachedEmailController.saveEmailAsInvoice
);
// Anhang als Rechnung speichern
// POST /api/emails/:id/attachments/:filename/save-as-invoice { invoiceDate, invoiceType, notes? }
router.post(
'/emails/:id/attachments/:filename/save-as-invoice',
authenticate,
requirePermission('contracts:update'),
cachedEmailController.saveAttachmentAsInvoice
);
// ==================== VERTRAGSZUORDNUNG ====================
// E-Mail Vertrag zuordnen
+54
View File
@@ -0,0 +1,54 @@
import { Router } from 'express';
import * as invoiceController from '../controllers/invoice.controller.js';
import { authenticate, requirePermission } from '../middleware/auth.js';
const router = Router();
// ==================== INVOICE CRUD ====================
// Alle Rechnungen für ein EnergyContractDetails abrufen
// GET /api/energy-details/:ecdId/invoices
router.get(
'/:ecdId/invoices',
authenticate,
requirePermission('contracts:read'),
invoiceController.getInvoices
);
// Einzelne Rechnung abrufen
// GET /api/energy-details/:ecdId/invoices/:invoiceId
router.get(
'/:ecdId/invoices/:invoiceId',
authenticate,
requirePermission('contracts:read'),
invoiceController.getInvoice
);
// Neue Rechnung hinzufügen
// POST /api/energy-details/:ecdId/invoices
router.post(
'/:ecdId/invoices',
authenticate,
requirePermission('contracts:update'),
invoiceController.addInvoice
);
// Rechnung aktualisieren
// PUT /api/energy-details/:ecdId/invoices/:invoiceId
router.put(
'/:ecdId/invoices/:invoiceId',
authenticate,
requirePermission('contracts:update'),
invoiceController.updateInvoice
);
// Rechnung löschen
// DELETE /api/energy-details/:ecdId/invoices/:invoiceId
router.delete(
'/:ecdId/invoices/:invoiceId',
authenticate,
requirePermission('contracts:delete'),
invoiceController.deleteInvoice
);
export default router;
+95
View File
@@ -658,4 +658,99 @@ router.delete(
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath')
);
// ==================== RECHNUNGS-DOKUMENTE ====================
// Upload für Rechnungs-Dokument
router.post(
'/invoices/:id',
authenticate,
requirePermission('contracts:update'),
setUploadDir('invoices'),
upload.single('document'),
async (req: AuthRequest, res: Response) => {
try {
if (!req.file) {
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
return;
}
const invoiceId = parseInt(req.params.id);
const relativePath = `/uploads/invoices/${req.file.filename}`;
// Alte Datei löschen falls vorhanden
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
if (!invoice) {
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
return;
}
if (invoice.documentPath) {
const oldPath = path.join(process.cwd(), invoice.documentPath);
if (fs.existsSync(oldPath)) {
fs.unlinkSync(oldPath);
}
}
// Invoice in der DB aktualisieren
await prisma.invoice.update({
where: { id: invoiceId },
data: { documentPath: relativePath },
});
res.json({
success: true,
data: {
path: relativePath,
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
},
});
} catch (error) {
console.error('Invoice upload error:', error);
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
}
}
);
// Löschen von Rechnungs-Dokument
router.delete(
'/invoices/:id',
authenticate,
requirePermission('contracts:update'),
async (req: AuthRequest, res: Response) => {
try {
const invoiceId = parseInt(req.params.id);
const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } });
if (!invoice) {
res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' });
return;
}
if (!invoice.documentPath) {
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
return;
}
// Datei löschen
const filePath = path.join(process.cwd(), invoice.documentPath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
// documentPath in DB auf null setzen
await prisma.invoice.update({
where: { id: invoiceId },
data: { documentPath: null },
});
res.json({ success: true });
} catch (error) {
console.error('Invoice delete error:', error);
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
}
}
);
export default router;
+2 -2
View File
@@ -124,14 +124,14 @@ export async function getContractById(id: number, decryptPassword = false) {
contractCategory: true,
previousContract: {
include: {
energyDetails: { include: { meter: { include: { readings: true } } } },
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
carInsuranceDetails: true,
},
},
energyDetails: { include: { meter: { include: { readings: true } } } },
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
internetDetails: { include: { phoneNumbers: true } },
mobileDetails: { include: { simCards: true } },
tvDetails: true,
@@ -49,6 +49,7 @@ export interface CockpitSummary {
contractEnding: number;
missingCredentials: number;
missingData: number;
missingInvoices: number;
openTasks: number;
pendingContracts: number;
};
@@ -142,11 +143,11 @@ export async function getCockpitData(): Promise<CockpitResult> {
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
const okDays = parseInt(settings.deadlineOkDays) || 90;
// Lade alle aktiven/pending Verträge mit allen relevanten Daten
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
const contracts = await prisma.contract.findMany({
where: {
status: {
in: ['ACTIVE', 'PENDING', 'DRAFT'],
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'],
},
},
include: {
@@ -182,6 +183,7 @@ export async function getCockpitData(): Promise<CockpitResult> {
energyDetails: {
include: {
meter: true,
invoices: true,
},
},
internetDetails: {
@@ -224,6 +226,7 @@ export async function getCockpitData(): Promise<CockpitResult> {
contractEnding: 0,
missingCredentials: 0,
missingData: 0,
missingInvoices: 0,
openTasks: 0,
pendingContracts: 0,
},
@@ -426,6 +429,75 @@ export async function getCockpitData(): Promise<CockpitResult> {
summary.byCategory.pendingContracts++;
}
// 13. ENERGIE-RECHNUNGEN (nur für ELECTRICITY und GAS)
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
const invoices = contract.energyDetails.invoices || [];
const now = new Date();
now.setHours(0, 0, 0, 0);
// 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag gekündigt/deaktiviert ist)
// "Beendet" = CANCELLED oder DEACTIVATED (nicht nur Laufzeit abgelaufen!)
const isContractTerminated = contract.status === 'CANCELLED' || contract.status === 'DEACTIVATED';
if (isContractTerminated) {
const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL');
const hasNotAvailable = invoices.some(inv => inv.invoiceType === 'NOT_AVAILABLE');
if (!hasFinalInvoice && !hasNotAvailable) {
issues.push({
type: 'missing_final_invoice',
label: 'Schlussrechnung fehlt',
urgency: 'warning',
details: 'Vertrag gekündigt/deaktiviert, aber keine Schlussrechnung vorhanden',
});
summary.byCategory.missingInvoices++;
}
}
// 13b. ZWISCHENRECHNUNG FEHLT/ÜBERFÄLLIG (wenn Vertrag > 12 Monate läuft)
// Für alle Status außer DRAFT und nicht gekündigt/deaktiviert
// Auch EXPIRED zählt hier, da der Vertrag ohne Kündigung weiterläuft!
if (contract.startDate && contract.status !== 'DRAFT' && !isContractTerminated) {
const startDate = new Date(contract.startDate);
startDate.setHours(0, 0, 0, 0);
const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysSinceStart > 365) {
// Vertrag läuft > 12 Monate
if (invoices.length === 0) {
// Keine Rechnungen vorhanden
issues.push({
type: 'missing_interim_invoice',
label: 'Zwischenrechnung fehlt',
urgency: 'warning',
details: 'Vertrag läuft über 12 Monate ohne Rechnung',
});
summary.byCategory.missingInvoices++;
} else {
// Prüfen ob letzte Rechnung > 12 Monate alt
const latestInvoice = invoices
.filter(inv => inv.invoiceType !== 'NOT_AVAILABLE')
.sort((a, b) => new Date(b.invoiceDate).getTime() - new Date(a.invoiceDate).getTime())[0];
if (latestInvoice) {
const invoiceDate = new Date(latestInvoice.invoiceDate);
const daysSinceInvoice = Math.floor((now.getTime() - invoiceDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysSinceInvoice > 365) {
issues.push({
type: 'overdue_interim_invoice',
label: 'Zwischenrechnung überfällig',
urgency: 'warning',
details: `Letzte Rechnung vor ${Math.floor(daysSinceInvoice / 30)} Monaten`,
});
summary.byCategory.missingInvoices++;
}
}
}
}
}
}
// Nur Verträge mit Issues hinzufügen
if (issues.length > 0) {
const highestUrgency = getHighestUrgency(issues);
+157
View File
@@ -0,0 +1,157 @@
import { PrismaClient, InvoiceType } from '@prisma/client';
import fs from 'fs';
import path from 'path';
const prisma = new PrismaClient();
export interface CreateInvoiceData {
invoiceDate: Date;
invoiceType: InvoiceType;
documentPath?: string;
notes?: string;
}
export interface UpdateInvoiceData {
invoiceDate?: Date;
invoiceType?: InvoiceType;
documentPath?: string;
notes?: string;
}
/**
* Alle Rechnungen für ein EnergyContractDetails abrufen
*/
export async function getInvoices(energyContractDetailsId: number) {
return prisma.invoice.findMany({
where: { energyContractDetailsId },
orderBy: { invoiceDate: 'desc' },
});
}
/**
* Einzelne Rechnung abrufen
*/
export async function getInvoice(energyContractDetailsId: number, invoiceId: number) {
return prisma.invoice.findFirst({
where: { id: invoiceId, energyContractDetailsId },
});
}
/**
* Neue Rechnung hinzufügen
*/
export async function addInvoice(energyContractDetailsId: number, data: CreateInvoiceData) {
// Validierung: documentPath ist Pflicht, außer bei NOT_AVAILABLE
if (data.invoiceType !== 'NOT_AVAILABLE' && !data.documentPath) {
throw new Error('Dokument ist Pflicht (außer bei Typ "Nicht verfügbar")');
}
// Prüfen ob EnergyContractDetails existiert
const energyDetails = await prisma.energyContractDetails.findUnique({
where: { id: energyContractDetailsId },
});
if (!energyDetails) {
throw new Error('Energievertrag nicht gefunden');
}
return prisma.invoice.create({
data: {
energyContractDetailsId,
invoiceDate: data.invoiceDate,
invoiceType: data.invoiceType,
documentPath: data.documentPath,
notes: data.notes,
},
});
}
/**
* Rechnung aktualisieren
*/
export async function updateInvoice(
energyContractDetailsId: number,
invoiceId: number,
data: UpdateInvoiceData
) {
// Prüfen ob Rechnung existiert und zum EnergyContractDetails gehört
const invoice = await prisma.invoice.findFirst({
where: { id: invoiceId, energyContractDetailsId },
});
if (!invoice) {
throw new Error('Rechnung nicht gefunden');
}
// Validierung bei Typ-Änderung
const newType = data.invoiceType ?? invoice.invoiceType;
const newPath = data.documentPath !== undefined ? data.documentPath : invoice.documentPath;
if (newType !== 'NOT_AVAILABLE' && !newPath) {
throw new Error('Dokument ist Pflicht (außer bei Typ "Nicht verfügbar")');
}
return prisma.invoice.update({
where: { id: invoiceId },
data: {
invoiceDate: data.invoiceDate,
invoiceType: data.invoiceType,
documentPath: data.documentPath,
notes: data.notes,
},
});
}
/**
* Rechnung löschen
*/
export async function deleteInvoice(energyContractDetailsId: number, invoiceId: number) {
const invoice = await prisma.invoice.findFirst({
where: { id: invoiceId, energyContractDetailsId },
});
if (!invoice) {
throw new Error('Rechnung nicht gefunden');
}
// Datei löschen falls vorhanden
if (invoice.documentPath) {
const filePath = path.join(process.cwd(), invoice.documentPath);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
return prisma.invoice.delete({ where: { id: invoiceId } });
}
/**
* Rechnung direkt erstellen (für E-Mail-Integration)
* Erstellt eine Rechnung mit bereits vorhandenem Dokument
*/
export async function createInvoiceWithDocument(
energyContractDetailsId: number,
invoiceDate: Date,
invoiceType: InvoiceType,
documentPath: string,
notes?: string
) {
// Prüfen ob EnergyContractDetails existiert
const energyDetails = await prisma.energyContractDetails.findUnique({
where: { id: energyContractDetailsId },
});
if (!energyDetails) {
throw new Error('Energievertrag nicht gefunden');
}
return prisma.invoice.create({
data: {
energyContractDetailsId,
invoiceDate,
invoiceType,
documentPath,
notes,
},
});
}