feat(posteingang): Duplikat als neue Version oder verwerfen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-11-02 13:58:31 +01:00
1 parent 170eb72c70
commit bdc85f5f1f
6 files changed
+166 -74

No files matched your search

@@ -18,6 +18,11 @@ import {
extractSectionToTemp,
} from './edit-applier';
import { applyTemplate, buildVariables } from './variable-resolver';
import { DokumentVersionService } from '../paperless/dokument-version.service';
import {
aktionenAbEntscheidung,
type PaperlessEntscheidung,
} from './paperless-entscheidung.util';
function parseFlexDate(s: string): Date | null {
if (!s) return null;
@@ -45,6 +50,12 @@ export interface ActionResult {
skipped?: boolean;
message?: string;
duplicateOfDocumentId?: number;
/** Duplikat mit genau einem Stammdokument → neue Version möglich */
versionMoeglich?: boolean;
/** Alle gefundenen Stammdokumente (bei mehrdeutigen Treffern) */
duplikatKandidaten?: number[];
/** Der Benutzer hat das Duplikat verworfen */
verworfen?: boolean;
}
export interface RunForDocumentResult {
@@ -56,6 +67,8 @@ interface PaperlessRunResult {
skipped?: boolean;
message?: string;
duplicateOfDocumentId?: number;
versionMoeglich?: boolean;
duplikatKandidaten?: number[];
}
@Injectable()
@@ -65,6 +78,7 @@ export class InboxPostprocessorService {
constructor(
private readonly pageCache: PageCacheService,
private readonly paperlessService: PaperlessService,
private readonly dokumentVersion: DokumentVersionService,
private readonly mailService: MailService,
private readonly exportService: ExportService,
@InjectRepository(InboxPostprocessingAction)
@@ -82,7 +96,7 @@ export class InboxPostprocessorService {
preferredUsername: string | null,
sectionOffset: number = 0,
processOnlyOne: boolean = false,
replaceDuplicate: boolean = false,
entscheidung?: PaperlessEntscheidung,
): Promise<RunForDocumentResult> {
const doc = await this.docRepo.findOne({ where: { Id: documentId } });
if (!doc) throw new NotFoundException('Dokument nicht gefunden');
@@ -252,16 +266,31 @@ export class InboxPostprocessorService {
: undefined;
try {
for (const action of tplActions) {
for (const action of aktionenAbEntscheidung(
tplActions,
entscheidung,
)) {
if (abortProcessing) break;
if (action.ActionType === 'PAPERLESS') {
if (
action.ActionType === 'PAPERLESS' &&
entscheidung?.aktion === 'verwerfen'
) {
results.push({
sectionIndex: currentSectionIndex,
actionId: action.Id,
actionType: action.ActionType,
ok: true,
verworfen: true,
message: 'Verworfen',
});
} else if (action.ActionType === 'PAPERLESS') {
try {
const res = await this.runPaperless(
action.Content ?? {},
sectionPdfPath,
variables,
defaultFilenameBase,
replaceDuplicate,
entscheidung,
);
results.push({
sectionIndex: currentSectionIndex,
@@ -271,6 +300,8 @@ export class InboxPostprocessorService {
skipped: res.skipped,
message: res.message,
duplicateOfDocumentId: res.duplicateOfDocumentId,
versionMoeglich: res.versionMoeglich,
duplikatKandidaten: res.duplikatKandidaten,
});
if (res.skipped) {
abortProcessing = true;
@@ -404,7 +435,7 @@ export class InboxPostprocessorService {
pdfPath: string,
variables: Record<string, string>,
defaultFilenameBase?: string,
replaceDuplicate: boolean = false,
entscheidung?: PaperlessEntscheidung,
): Promise<PaperlessRunResult> {
// 1. Interne Belegnummer auflösen (Pflicht)
const intNrTpl = String(content.interneBelegnummer ?? '').trim();
@@ -416,7 +447,21 @@ export class InboxPostprocessorService {
if (!interneBelegnummer)
throw new Error('Interne Belegnummer konnte nicht aufgelöst werden');
// 2. ASN bestimmen (vor Duplikat-Checks, damit Paperless danach gefragt werden kann)
// 2. Vom Benutzer bestätigt: als neue Version an das Stammdokument hängen
if (entscheidung?.aktion === 'version') {
await this.dokumentVersion.haengeVersionAn(
entscheidung.documentId,
pdfPath,
);
this.logger.log(
`Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
);
return {
message: `Als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
};
}
// 3. ASN bestimmen (für den Upload)
const asnTpl = String(content.asn ?? '').trim();
const asn = asnTpl ? applyTemplate(asnTpl, variables).trim() : null;
let archiveSerialNumber: number | undefined;
@@ -429,71 +474,52 @@ export class InboxPostprocessorService {
if (!Number.isNaN(n)) archiveSerialNumber = n;
}
if (replaceDuplicate) {
this.logger.log(
`Duplikat-Check übersprungen (replaceDuplicate=true) für ${interneBelegnummer}`,
// 4. Duplikat-Check Paperless: CF7 + aus der Belegnummer abgeleitete ASN
const pruefung =
await this.paperlessService.pruefeBelegnummer(interneBelegnummer);
if (pruefung.status !== 'frei') {
this.logger.warn(
`Duplikat (Paperless): Belegnummer ${interneBelegnummer} bereits vorhanden (Dokument(e) ${pruefung.kandidaten.join(', ')})`,
);
} else {
// 3. Duplikat-Check lokal (tasks-Tabelle)
const existingTask = await this.taskRepo.findOneBy({
InterneBelegnummer: interneBelegnummer,
});
if (existingTask) {
this.logger.warn(
`Duplikat (lokal): Belegnummer ${interneBelegnummer} bereits in tasks-Tabelle`,
);
return {
skipped: true,
message: `Duplikat – Belegnummer ${interneBelegnummer} bereits vorhanden`,
duplicateOfDocumentId: existingTask.PaperlessDocumentID ?? undefined,
};
}
return {
skipped: true,
message:
pruefung.status === 'eindeutig'
? `Duplikat – Belegnummer ${interneBelegnummer} bereits in Paperless`
: `Duplikat – Belegnummer ${interneBelegnummer} mehrfach in Paperless`,
duplicateOfDocumentId: pruefung.documentId ?? pruefung.kandidaten[0],
versionMoeglich: pruefung.status === 'eindeutig',
duplikatKandidaten: pruefung.kandidaten,
};
}
// 4. Duplikat-Check Paperless API (Custom Field 7 = InterneBelegnummer)
const cf7DocId = await this.paperlessService.findDocumentIdByCustomField(
7,
interneBelegnummer,
// 5. Duplikat-Check lokal (tasks-Tabelle)
const existingTask = await this.taskRepo.findOneBy({
InterneBelegnummer: interneBelegnummer,
});
if (existingTask) {
this.logger.warn(
`Duplikat (lokal): Belegnummer ${interneBelegnummer} bereits in tasks-Tabelle`,
);
if (cf7DocId !== null) {
this.logger.warn(
`Duplikat (Paperless CF7): Belegnummer ${interneBelegnummer} bereits in Paperless (Doc ${cf7DocId})`,
);
return {
skipped: true,
message: `Duplikat – Belegnummer ${interneBelegnummer} bereits in Paperless`,
duplicateOfDocumentId: cf7DocId,
};
}
return {
skipped: true,
message: `Duplikat – Belegnummer ${interneBelegnummer} bereits vorhanden`,
duplicateOfDocumentId: existingTask.PaperlessDocumentID ?? undefined,
versionMoeglich: false,
};
}
// 5. Duplikat-Check Paperless API (archive_serial_number)
if (archiveSerialNumber !== undefined) {
const asnDocId =
await this.paperlessService.findDocumentIdByAsn(archiveSerialNumber);
if (asnDocId !== null) {
this.logger.warn(
`Duplikat (Paperless ASN): ${archiveSerialNumber} bereits in Paperless (Doc ${asnDocId})`,
);
return {
skipped: true,
message: `Duplikat – ASN ${archiveSerialNumber} bereits in Paperless`,
duplicateOfDocumentId: asnDocId,
};
}
}
// 6. Checksum berechnen und prüfen
const buffer = await fs.readFile(pdfPath);
// Paperless ≥ 3.0 vergleicht SHA-256-Checksummen
const checksum = crypto.createHash('sha256').update(buffer).digest('hex');
const checksumExists =
await this.paperlessService.checksumExists(checksum);
if (checksumExists) {
this.logger.warn(`Duplikat (Checksum): ${checksum}`);
return {
skipped: true,
message: 'Duplikat (Checksum-Übereinstimmung)',
};
}
// 6. Checksum berechnen und prüfen
const buffer = await fs.readFile(pdfPath);
// Paperless ≥ 3.0 vergleicht SHA-256-Checksummen
const checksum = crypto.createHash('sha256').update(buffer).digest('hex');
const checksumExists = await this.paperlessService.checksumExists(checksum);
if (checksumExists) {
this.logger.warn(`Duplikat (Checksum): ${checksum}`);
return {
skipped: true,
message: 'Duplikat (Checksum-Übereinstimmung)',
};
}
// 7. Restliche Metadaten auflösen