diff --git a/paperless-backend/src/paperless/paperless-processor.service.ts b/paperless-backend/src/paperless/paperless-processor.service.ts index 9a54b6f..54d0913 100644 --- a/paperless-backend/src/paperless/paperless-processor.service.ts +++ b/paperless-backend/src/paperless/paperless-processor.service.ts @@ -1,5 +1,4 @@ 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'; @@ -8,6 +7,8 @@ import { DocumentType } from '../database/entities/document-type.entity'; import { PaperlessService } from './paperless.service'; import { PostprocessingService } from '../postprocessing/postprocessing.service'; +const PAPERLESSMANAGER_TAG_ID = 16; // Tag "paperlessmanager" + @Injectable() export class PaperlessProcessorService { private readonly logger = new Logger(PaperlessProcessorService.name); @@ -22,11 +23,13 @@ export class PaperlessProcessorService { private readonly docFieldRepo: Repository, ) {} - @Cron(process.env.PAPERLESS_PROCESSOR_CRON || '0 * * * * *') + // Manueller Batch-Lauf ("alle paperlessmanager-Dokumente neu durchlaufen"). + // Die laufende Verarbeitung erfolgt ereignisgesteuert über den Webhook + // (processDocumentById), daher ist hier kein @Cron-Trigger mehr gesetzt. async processDocuments() { try { const response = await this.paperlessService.getDocuments({ - tags__id__all: 16, + tags__id__all: PAPERLESSMANAGER_TAG_ID, page_size: 9999, }); const documents: any[] = Array.isArray(response) @@ -38,17 +41,12 @@ export class PaperlessProcessorService { const validFieldIds = new Set(customFields.map((f: any) => f.id)); this.logger.log( - `Verarbeite ${documents.length} Dokument(e) mit Tag "paperlessmanager" (ID: 16).`, + `Verarbeite ${documents.length} Dokument(e) mit Tag "paperlessmanager" (ID: ${PAPERLESSMANAGER_TAG_ID}).`, ); for (const doc of documents) { try { - const updatedDoc = await this.processSingleDocument( - doc, - validFieldIds, - ); - // Postprocessing nach dem Speichern evaluieren - await this.postprocessingService.evaluate(updatedDoc || doc); + await this.processAndEvaluate(doc, validFieldIds); } catch (innerErr: any) { this.logger.error( `Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`, @@ -65,6 +63,45 @@ export class PaperlessProcessorService { } } + /** + * Verarbeitet ein einzelnes Dokument anhand seiner ID – die ereignisgesteuerte + * Variante des früheren Cron-Jobs (vom Paperless-Webhook aufgerufen). Es wird + * nur verarbeitet, wenn das Dokument den Tag "paperlessmanager" trägt. + */ + async processDocumentById( + documentId: number, + ): Promise<{ processed: boolean; reason?: string }> { + const doc = await this.paperlessService.getDocument(documentId); + const tags: number[] = doc.tags || []; + if (!tags.includes(PAPERLESSMANAGER_TAG_ID)) { + this.logger.log( + `Dokument ${documentId} ohne Tag "paperlessmanager" (ID ${PAPERLESSMANAGER_TAG_ID}) – übersprungen.`, + ); + return { processed: false, reason: 'tag-missing' }; + } + + const customFields = await this.paperlessService.getCustomFields(); + const validFieldIds = new Set(customFields.map((f: any) => f.id)); + + await this.processAndEvaluate(doc, validFieldIds); + this.logger.log(`Dokument ${documentId} per Webhook verarbeitet.`); + return { processed: true }; + } + + /** + * Reichert ein Dokument an (Tags, Pflichtfelder, Titel) und evaluiert + * anschließend das Postprocessing. Gemeinsame Logik von Batch-Lauf und Webhook. + */ + private async processAndEvaluate( + doc: any, + validFieldIds: Set, + ): Promise { + const updatedDoc = await this.processSingleDocument(doc, validFieldIds); + // Postprocessing nach dem Speichern evaluieren + await this.postprocessingService.evaluate(updatedDoc || doc); + return updatedDoc; + } + private async processSingleDocument( doc: any, validFieldIds: Set, diff --git a/paperless-backend/src/paperless/paperless.module.ts b/paperless-backend/src/paperless/paperless.module.ts index eb5db29..6b6f4c6 100644 --- a/paperless-backend/src/paperless/paperless.module.ts +++ b/paperless-backend/src/paperless/paperless.module.ts @@ -32,6 +32,6 @@ import { AuthModule } from '../auth/auth.module'; PaperlessProcessorService, PaperlessTaskProcessorService, ], - exports: [PaperlessService], + exports: [PaperlessService, PaperlessProcessorService], }) export class PaperlessModule {} diff --git a/paperless-backend/src/webhook/webhook.controller.ts b/paperless-backend/src/webhook/webhook.controller.ts index 8154711..0efe857 100644 --- a/paperless-backend/src/webhook/webhook.controller.ts +++ b/paperless-backend/src/webhook/webhook.controller.ts @@ -5,32 +5,78 @@ import { Logger, HttpCode, HttpStatus, + UseGuards, + BadRequestException, } from '@nestjs/common'; -import { Public } from '../auth/public.decorator'; +import { ApiKeyGuard } from '../auth/api-key.guard'; +import { PaperlessProcessorService } from '../paperless/paperless-processor.service'; + export interface PaperlessWebhookPayload { - document_id: number; - action: string; + doc_url?: string; + document_id?: number; // optional, für manuelles Testen + action?: string; [key: string]: any; } -@Controller('webhook') +@Controller('api/webhook') export class WebhookController { private readonly logger = new Logger(WebhookController.name); - @Public() + constructor(private readonly paperlessProcessor: PaperlessProcessorService) {} + + @UseGuards(ApiKeyGuard) @Post('paperless') @HttpCode(HttpStatus.OK) - async handlePaperlessWebhook( - @Body() payload: PaperlessWebhookPayload, - ): Promise<{ status: string }> { + async handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload) { + const documentId = this.extractDocumentId(payload); + if (!documentId) { + this.logger.warn( + `Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`, + ); + throw new BadRequestException( + 'Keine Dokument-ID aus doc_url/document_id ermittelbar', + ); + } + this.logger.log( - `Webhook empfangen: action=${payload.action}, document=${payload.document_id}`, + `Webhook: action=${payload.action}, document=${documentId}`, ); - // TODO: Business-Logik für verschiedene Webhook-Events - // - document_updated → Felder prüfen, Postprocessing auslösen - // - document_consumed → GoBD-Archivierung prüfen + try { + const result = + await this.paperlessProcessor.processDocumentById(documentId); + return { + status: result.processed ? 'processed' : 'skipped', + reason: result.reason, + }; + } catch (err) { + // Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben, + // damit Paperless keine Retry-Schleife startet. + const message = err instanceof Error ? err.message : String(err); + this.logger.error( + `Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`, + ); + return { status: 'error', message }; + } + } - return { status: 'received' }; + /** + * Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das + * optionale Feld `document_id` (für manuelles Testen), andernfalls wird die + * ID aus dem Paperless-Platzhalter `{{doc_url}}` extrahiert + * (z.B. ".../documents/123/"). + */ + private extractDocumentId(payload: PaperlessWebhookPayload): number | null { + if ( + payload?.document_id != null && + !Number.isNaN(Number(payload.document_id)) + ) { + return Number(payload.document_id); + } + if (typeof payload?.doc_url === 'string') { + const m = payload.doc_url.match(/\/documents\/(\d+)/); + if (m) return Number(m[1]); + } + return null; } } diff --git a/paperless-backend/src/webhook/webhook.module.ts b/paperless-backend/src/webhook/webhook.module.ts index b004812..466eb2e 100644 --- a/paperless-backend/src/webhook/webhook.module.ts +++ b/paperless-backend/src/webhook/webhook.module.ts @@ -1,7 +1,10 @@ import { Module } from '@nestjs/common'; import { WebhookController } from './webhook.controller'; +import { PaperlessModule } from '../paperless/paperless.module'; +import { AuthModule } from '../auth/auth.module'; @Module({ + imports: [PaperlessModule, AuthModule], controllers: [WebhookController], }) export class WebhookModule {}