Freigabe → main: Mobile UI, Webhook, IMAP, Zahlung-Workflow und weitere Features #5
@@ -1,5 +1,4 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron } from '@nestjs/schedule';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
@@ -8,6 +7,8 @@ import { DocumentType } from '../database/entities/document-type.entity';
|
|||||||
import { PaperlessService } from './paperless.service';
|
import { PaperlessService } from './paperless.service';
|
||||||
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
||||||
|
|
||||||
|
const PAPERLESSMANAGER_TAG_ID = 16; // Tag "paperlessmanager"
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaperlessProcessorService {
|
export class PaperlessProcessorService {
|
||||||
private readonly logger = new Logger(PaperlessProcessorService.name);
|
private readonly logger = new Logger(PaperlessProcessorService.name);
|
||||||
@@ -22,11 +23,13 @@ export class PaperlessProcessorService {
|
|||||||
private readonly docFieldRepo: Repository<DocumentField>,
|
private readonly docFieldRepo: Repository<DocumentField>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@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() {
|
async processDocuments() {
|
||||||
try {
|
try {
|
||||||
const response = await this.paperlessService.getDocuments({
|
const response = await this.paperlessService.getDocuments({
|
||||||
tags__id__all: 16,
|
tags__id__all: PAPERLESSMANAGER_TAG_ID,
|
||||||
page_size: 9999,
|
page_size: 9999,
|
||||||
});
|
});
|
||||||
const documents: any[] = Array.isArray(response)
|
const documents: any[] = Array.isArray(response)
|
||||||
@@ -38,17 +41,12 @@ export class PaperlessProcessorService {
|
|||||||
const validFieldIds = new Set(customFields.map((f: any) => f.id));
|
const validFieldIds = new Set(customFields.map((f: any) => f.id));
|
||||||
|
|
||||||
this.logger.log(
|
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) {
|
for (const doc of documents) {
|
||||||
try {
|
try {
|
||||||
const updatedDoc = await this.processSingleDocument(
|
await this.processAndEvaluate(doc, validFieldIds);
|
||||||
doc,
|
|
||||||
validFieldIds,
|
|
||||||
);
|
|
||||||
// Postprocessing nach dem Speichern evaluieren
|
|
||||||
await this.postprocessingService.evaluate(updatedDoc || doc);
|
|
||||||
} catch (innerErr: any) {
|
} catch (innerErr: any) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`,
|
`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<number>(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<number>,
|
||||||
|
): Promise<any> {
|
||||||
|
const updatedDoc = await this.processSingleDocument(doc, validFieldIds);
|
||||||
|
// Postprocessing nach dem Speichern evaluieren
|
||||||
|
await this.postprocessingService.evaluate(updatedDoc || doc);
|
||||||
|
return updatedDoc;
|
||||||
|
}
|
||||||
|
|
||||||
private async processSingleDocument(
|
private async processSingleDocument(
|
||||||
doc: any,
|
doc: any,
|
||||||
validFieldIds: Set<number>,
|
validFieldIds: Set<number>,
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ import { AuthModule } from '../auth/auth.module';
|
|||||||
PaperlessProcessorService,
|
PaperlessProcessorService,
|
||||||
PaperlessTaskProcessorService,
|
PaperlessTaskProcessorService,
|
||||||
],
|
],
|
||||||
exports: [PaperlessService],
|
exports: [PaperlessService, PaperlessProcessorService],
|
||||||
})
|
})
|
||||||
export class PaperlessModule {}
|
export class PaperlessModule {}
|
||||||
|
|||||||
@@ -5,32 +5,78 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} 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 {
|
export interface PaperlessWebhookPayload {
|
||||||
document_id: number;
|
doc_url?: string;
|
||||||
action: string;
|
document_id?: number; // optional, für manuelles Testen
|
||||||
|
action?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('webhook')
|
@Controller('api/webhook')
|
||||||
export class WebhookController {
|
export class WebhookController {
|
||||||
private readonly logger = new Logger(WebhookController.name);
|
private readonly logger = new Logger(WebhookController.name);
|
||||||
|
|
||||||
@Public()
|
constructor(private readonly paperlessProcessor: PaperlessProcessorService) {}
|
||||||
|
|
||||||
|
@UseGuards(ApiKeyGuard)
|
||||||
@Post('paperless')
|
@Post('paperless')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async handlePaperlessWebhook(
|
async handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload) {
|
||||||
@Body() payload: PaperlessWebhookPayload,
|
const documentId = this.extractDocumentId(payload);
|
||||||
): Promise<{ status: string }> {
|
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(
|
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
|
try {
|
||||||
// - document_updated → Felder prüfen, Postprocessing auslösen
|
const result =
|
||||||
// - document_consumed → GoBD-Archivierung prüfen
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { WebhookController } from './webhook.controller';
|
import { WebhookController } from './webhook.controller';
|
||||||
|
import { PaperlessModule } from '../paperless/paperless.module';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [PaperlessModule, AuthModule],
|
||||||
controllers: [WebhookController],
|
controllers: [WebhookController],
|
||||||
})
|
})
|
||||||
export class WebhookModule {}
|
export class WebhookModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user