import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { DocumentField } from '../database/entities/document-field.entity'; import { DocumentType } from '../database/entities/document-type.entity'; import { PaperlessService } from './paperless.service'; import { PostprocessingService } from '../postprocessing/postprocessing.service'; @Injectable() export class PaperlessProcessorService { private readonly logger = new Logger(PaperlessProcessorService.name); constructor( private readonly configService: ConfigService, private readonly paperlessService: PaperlessService, private readonly postprocessingService: PostprocessingService, @InjectRepository(DocumentType) private readonly docTypeRepo: Repository, @InjectRepository(DocumentField) private readonly docFieldRepo: Repository, ) {} @Cron(process.env.PAPERLESS_PROCESSOR_CRON || '0 * * * * *') async processDocuments() { try { const response = await this.paperlessService.getDocuments({ tags__id__all: 16, page_size: 9999, }); const documents: any[] = Array.isArray(response) ? response : (response?.results ?? []); if (documents.length === 0) return; const customFields = await this.paperlessService.getCustomFields(); const validFieldIds = new Set(customFields.map((f: any) => f.id)); this.logger.log( `Verarbeite ${documents.length} Dokument(e) mit Tag "paperlessmanager" (ID: 16).`, ); for (const doc of documents) { try { const updatedDoc = await this.processSingleDocument( doc, validFieldIds, ); // Postprocessing nach dem Speichern evaluieren await this.postprocessingService.evaluate(updatedDoc || doc); } catch (innerErr: any) { this.logger.error( `Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`, ); if (innerErr.response?.data) { this.logger.error( `Paperless API Response: ${JSON.stringify(innerErr.response.data)}`, ); } } } } catch (err) { this.logger.error('Fehler bei der Dokumentenverarbeitung:', err); } } private async processSingleDocument( doc: any, validFieldIds: Set, ): Promise { this.logger.log(`Verarbeite Dokument ID: ${doc.id}`); if (!doc.document_type) { this.logger.warn( `Dokument ${doc.id} hat keinen Dokumenten-Typen – setze Tag 17.`, ); const tagsSet = new Set(doc.tags || []); tagsSet.add(17); if (!tagsSet.has(1)) { tagsSet.add(6); } const updated = await this.paperlessService.updateDocument(doc.id, { tags: Array.from(tagsSet), }); return updated; } const docTypeConfig = await this.docTypeRepo.findOne({ where: { DocumentTypeId: doc.document_type }, }); if (!docTypeConfig) { this.logger.warn( `Konfiguration für DocumentType ${doc.document_type} nicht in der Datenbank gefunden.`, ); return null; } const fieldsConfig = await this.docFieldRepo.find({ where: { DocumentType: doc.document_type }, }); if (fieldsConfig.length === 0) { this.logger.log( `Dokument ${doc.id} (Typ ${doc.document_type}) hat keine Dokument-Felder in der DB konfiguriert.`, ); const newTagsNoFields = Array.from(new Set([...(doc.tags || []), 17])); const updated = await this.paperlessService.updateDocument(doc.id, { tags: newTagsNoFields, }); return updated; } const currentCustomFields = doc.custom_fields || []; const newCustomFields = [...currentCustomFields]; let isAllRequiredFilled = true; for (const fieldConf of fieldsConfig) { if (fieldConf.Type === 4) { const customFieldId = fieldConf.TypeIndex; if (!customFieldId) continue; if (!validFieldIds.has(customFieldId)) { this.logger.warn( `Überspringe ungültiges Custom Field (TypeIndex: ${customFieldId}) für Dokument ${doc.id} - in Paperless nicht vorhanden.`, ); continue; } const existingField = newCustomFields.find( (f) => f.field === customFieldId, ); let isFilled = false; if (existingField) { isFilled = existingField.value !== null && existingField.value !== ''; } else { newCustomFields.push({ field: customFieldId, value: null }); } if (fieldConf.IsRequired && !isFilled) { isAllRequiredFilled = false; } } else { if (fieldConf.IsRequired) { let isFilled = false; switch (fieldConf.Type) { case 1: isFilled = doc.correspondent !== null && doc.correspondent !== undefined; break; case 2: isFilled = !!doc.created || !!doc.created_date; break; case 3: isFilled = doc.archive_serial_number !== null && doc.archive_serial_number !== undefined; break; case 5: isFilled = !!doc.title; break; default: isFilled = true; } if (!isFilled) { isAllRequiredFilled = false; } } } } const tagsSet = new Set(doc.tags || []); if (isAllRequiredFilled) { if (docTypeConfig.TagReady) tagsSet.add(docTypeConfig.TagReady); if (docTypeConfig.TagNotReady) tagsSet.delete(docTypeConfig.TagNotReady); } else { if (docTypeConfig.TagNotReady) tagsSet.add(docTypeConfig.TagNotReady); if (docTypeConfig.TagReady) tagsSet.delete(docTypeConfig.TagReady); } tagsSet.add(17); // Titel-Template auflösen const updatePayload: any = { custom_fields: newCustomFields, tags: Array.from(tagsSet), }; if (docTypeConfig.TitelTemplate) { let title = docTypeConfig.TitelTemplate; // Prüfen ob alle im Template referenzierten Custom Fields ausgefüllt sind const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g; let allFilled = true; let match: RegExpExecArray | null; while ((match = placeholderRegex.exec(title)) !== null) { const fieldId = parseInt(match[1], 10); const cf = newCustomFields.find((f) => f.field === fieldId); if (!cf || cf.value == null || cf.value === '') { allFilled = false; break; } } if (allFilled) { for (const cf of newCustomFields) { const placeholder = `{{CUSTOM[${cf.field}]}}`; if (title.includes(placeholder)) { title = title.replaceAll( placeholder, cf.value != null ? String(cf.value) : '', ); } } if (title.includes('{{DATE}}')) { const created = doc.created ? new Date(doc.created) : new Date(); const dateStr = `${String(created.getDate()).padStart(2, '0')}.${String(created.getMonth() + 1).padStart(2, '0')}.${created.getFullYear()}`; title = title.replaceAll('{{DATE}}', dateStr); } if (title.includes('{{ZEITSTEMPEL}}')) { const now = new Date(); const ts = `${String(now.getDate()).padStart(2, '0')}.${String(now.getMonth() + 1).padStart(2, '0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; title = title.replaceAll('{{ZEITSTEMPEL}}', ts); } updatePayload.title = title; } else { this.logger.log( `Dokument ${doc.id}: Titel-Template nicht angewendet – nicht alle referenzierten Custom Fields ausgefüllt.`, ); } } const updated = await this.paperlessService.updateDocument( doc.id, updatePayload, ); this.logger.log( `Dokument ${doc.id} erfolgreich aktualisiert (Alle Pflichtfelder vorhanden: ${isAllRequiredFilled}).`, ); return updated; } }