feat: Duplikate im Import als neue Version behandeln #6
No files matched your search
@@ -237,6 +237,7 @@ describe('EmailImportService – Duplikatprüfung', () => {
|
|||||||
expect(dokumentVersion.haengeVersionAn).toHaveBeenCalledWith(
|
expect(dokumentVersion.haengeVersionAn).toHaveBeenCalledWith(
|
||||||
42,
|
42,
|
||||||
expect.stringMatching(/\.pdf$/),
|
expect.stringMatching(/\.pdf$/),
|
||||||
|
'2026-000123',
|
||||||
);
|
);
|
||||||
expect(paperlessService.uploadDocument).not.toHaveBeenCalled();
|
expect(paperlessService.uploadDocument).not.toHaveBeenCalled();
|
||||||
expect(paperlessService.validateAsnNotExists).not.toHaveBeenCalled();
|
expect(paperlessService.validateAsnNotExists).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ export class EmailImportService {
|
|||||||
*/
|
*/
|
||||||
private async importiereAlsVersion(
|
private async importiereAlsVersion(
|
||||||
documentId: number,
|
documentId: number,
|
||||||
|
belegnummer: string,
|
||||||
uploadItems: { path: string; filename: string; rangeKey: string }[],
|
uploadItems: { path: string; filename: string; rangeKey: string }[],
|
||||||
paperlessIds: Record<string, number>,
|
paperlessIds: Record<string, number>,
|
||||||
jobId?: string,
|
jobId?: string,
|
||||||
@@ -653,7 +654,11 @@ export class EmailImportService {
|
|||||||
jobId,
|
jobId,
|
||||||
`Hänge ${item.filename} als neue Version an Dokument ${documentId}...`,
|
`Hänge ${item.filename} als neue Version an Dokument ${documentId}...`,
|
||||||
);
|
);
|
||||||
await this.dokumentVersion.haengeVersionAn(documentId, item.path);
|
await this.dokumentVersion.haengeVersionAn(
|
||||||
|
documentId,
|
||||||
|
item.path,
|
||||||
|
belegnummer,
|
||||||
|
);
|
||||||
paperlessIds[item.rangeKey] = documentId;
|
paperlessIds[item.rangeKey] = documentId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,6 +770,7 @@ export class EmailImportService {
|
|||||||
if (att.versionZuDokumentId) {
|
if (att.versionZuDokumentId) {
|
||||||
await this.importiereAlsVersion(
|
await this.importiereAlsVersion(
|
||||||
att.versionZuDokumentId,
|
att.versionZuDokumentId,
|
||||||
|
att.belegnummer ?? '',
|
||||||
uploadPromises,
|
uploadPromises,
|
||||||
paperlessIds,
|
paperlessIds,
|
||||||
data.jobId,
|
data.jobId,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { InboxPostprocessorService } from './inbox-postprocessor.service';
|
||||||
|
import { originalSeitenDesAbschnitts } from './paperless-entscheidung.util';
|
||||||
|
|
||||||
|
describe('InboxPostprocessorService.runForDocument', () => {
|
||||||
|
it('nimmt eine Duplikat-Entscheidung nur für genau einen Abschnitt an', async () => {
|
||||||
|
const docRepo = { findOne: jest.fn() };
|
||||||
|
const service = new (InboxPostprocessorService as unknown as new (
|
||||||
|
...args: unknown[]
|
||||||
|
) => InboxPostprocessorService)({}, {}, {}, {}, {}, {}, docRepo, {}, {});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.runForDocument('doc-1', null, 0, false, {
|
||||||
|
aktion: 'version',
|
||||||
|
documentId: 42,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
expect(docRepo.findOne).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('originalSeitenDesAbschnitts', () => {
|
||||||
|
it('bildet Indizes der bearbeiteten PDF auf Original-Seiten ab', () => {
|
||||||
|
// Seite 2 gelöscht: bearbeitete PDF = Original 1, 3, 4, 5
|
||||||
|
expect(originalSeitenDesAbschnitts([1, 3, 4, 5], [1, 2])).toEqual([3, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
@@ -21,6 +26,7 @@ import { applyTemplate, buildVariables } from './variable-resolver';
|
|||||||
import { DokumentVersionService } from '../paperless/dokument-version.service';
|
import { DokumentVersionService } from '../paperless/dokument-version.service';
|
||||||
import {
|
import {
|
||||||
aktionenAbEntscheidung,
|
aktionenAbEntscheidung,
|
||||||
|
originalSeitenDesAbschnitts,
|
||||||
type PaperlessEntscheidung,
|
type PaperlessEntscheidung,
|
||||||
} from './paperless-entscheidung.util';
|
} from './paperless-entscheidung.util';
|
||||||
|
|
||||||
@@ -58,6 +64,8 @@ export interface ActionResult {
|
|||||||
verworfen?: boolean;
|
verworfen?: boolean;
|
||||||
/** Aufgelöste interne Belegnummer (bei Duplikaten) */
|
/** Aufgelöste interne Belegnummer (bei Duplikaten) */
|
||||||
belegnummer?: string;
|
belegnummer?: string;
|
||||||
|
/** Original-Seiten des Abschnitts (bei Duplikaten, für den Vergleich) */
|
||||||
|
seiten?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunForDocumentResult {
|
export interface RunForDocumentResult {
|
||||||
@@ -101,6 +109,12 @@ export class InboxPostprocessorService {
|
|||||||
processOnlyOne: boolean = false,
|
processOnlyOne: boolean = false,
|
||||||
entscheidung?: PaperlessEntscheidung,
|
entscheidung?: PaperlessEntscheidung,
|
||||||
): Promise<RunForDocumentResult> {
|
): Promise<RunForDocumentResult> {
|
||||||
|
// Eine Duplikat-Entscheidung gilt immer nur für den einen Abschnitt
|
||||||
|
if (entscheidung && !processOnlyOne) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Eine Duplikat-Entscheidung ist nur zusammen mit processOnlyOne erlaubt',
|
||||||
|
);
|
||||||
|
}
|
||||||
const doc = await this.docRepo.findOne({ where: { Id: documentId } });
|
const doc = await this.docRepo.findOne({ where: { Id: documentId } });
|
||||||
if (!doc) throw new NotFoundException('Dokument nicht gefunden');
|
if (!doc) throw new NotFoundException('Dokument nicht gefunden');
|
||||||
if (doc.Source === 'user' && doc.OwnerUsername !== preferredUsername) {
|
if (doc.Source === 'user' && doc.OwnerUsername !== preferredUsername) {
|
||||||
@@ -306,6 +320,12 @@ export class InboxPostprocessorService {
|
|||||||
versionMoeglich: res.versionMoeglich,
|
versionMoeglich: res.versionMoeglich,
|
||||||
duplikatKandidaten: res.duplikatKandidaten,
|
duplikatKandidaten: res.duplikatKandidaten,
|
||||||
belegnummer: res.belegnummer,
|
belegnummer: res.belegnummer,
|
||||||
|
seiten: res.skipped
|
||||||
|
? originalSeitenDesAbschnitts(
|
||||||
|
survivingOriginalPages,
|
||||||
|
pageIndices,
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
if (res.skipped) {
|
if (res.skipped) {
|
||||||
abortProcessing = true;
|
abortProcessing = true;
|
||||||
@@ -456,6 +476,7 @@ export class InboxPostprocessorService {
|
|||||||
await this.dokumentVersion.haengeVersionAn(
|
await this.dokumentVersion.haengeVersionAn(
|
||||||
entscheidung.documentId,
|
entscheidung.documentId,
|
||||||
pdfPath,
|
pdfPath,
|
||||||
|
interneBelegnummer,
|
||||||
);
|
);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
|
`Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
|
||||||
|
|||||||
@@ -16,3 +16,14 @@ export function aktionenAbEntscheidung<T extends { ActionType: string }>(
|
|||||||
const start = aktionen.findIndex((a) => a.ActionType === 'PAPERLESS');
|
const start = aktionen.findIndex((a) => a.ActionType === 'PAPERLESS');
|
||||||
return start === -1 ? aktionen : aktionen.slice(start);
|
return start === -1 ? aktionen : aktionen.slice(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original-Seitennummern (1-basiert) eines Abschnitts: `pageIndices` sind
|
||||||
|
* 0-basierte Indizes in der bearbeiteten PDF (gelöschte Seiten fehlen dort).
|
||||||
|
*/
|
||||||
|
export function originalSeitenDesAbschnitts(
|
||||||
|
survivingOriginalPages: number[],
|
||||||
|
pageIndices: number[],
|
||||||
|
): number[] {
|
||||||
|
return pageIndices.map((idx) => survivingOriginalPages[idx]);
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
getDocument: jest.Mock;
|
getDocument: jest.Mock;
|
||||||
updateDocument: jest.Mock;
|
updateDocument: jest.Mock;
|
||||||
uploadDocumentVersion: jest.Mock;
|
uploadDocumentVersion: jest.Mock;
|
||||||
|
pruefeBelegnummer: jest.Mock;
|
||||||
|
getTask: jest.Mock;
|
||||||
};
|
};
|
||||||
let service: DokumentVersionService;
|
let service: DokumentVersionService;
|
||||||
|
|
||||||
@@ -25,16 +27,68 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
getDocument: jest.fn(),
|
getDocument: jest.fn(),
|
||||||
updateDocument: jest.fn().mockResolvedValue({}),
|
updateDocument: jest.fn().mockResolvedValue({}),
|
||||||
uploadDocumentVersion: jest.fn().mockResolvedValue('task-1'),
|
uploadDocumentVersion: jest.fn().mockResolvedValue('task-1'),
|
||||||
|
pruefeBelegnummer: jest.fn().mockResolvedValue({
|
||||||
|
status: 'eindeutig',
|
||||||
|
documentId: 42,
|
||||||
|
kandidaten: [42],
|
||||||
|
}),
|
||||||
|
getTask: jest.fn().mockResolvedValue({ status: 'SUCCESS' }),
|
||||||
};
|
};
|
||||||
service = new DokumentVersionService(
|
service = new DokumentVersionService(
|
||||||
paperless as unknown as PaperlessService,
|
paperless as unknown as PaperlessService,
|
||||||
);
|
);
|
||||||
|
jest
|
||||||
|
.spyOn(service as unknown as { warte: () => Promise<void> }, 'warte')
|
||||||
|
.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lehnt ab, wenn die Belegnummer nicht (mehr) eindeutig zu diesem Dokument gehört', async () => {
|
||||||
|
paperless.pruefeBelegnummer.mockResolvedValue({
|
||||||
|
status: 'eindeutig',
|
||||||
|
documentId: 7,
|
||||||
|
kandidaten: [7],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123'),
|
||||||
|
).rejects.toThrow(/Dokument 42/);
|
||||||
|
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
||||||
|
expect(paperless.uploadDocumentVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wartet auf die Paperless-Verarbeitung der Version', async () => {
|
||||||
|
paperless.getDocument.mockResolvedValue({ id: 42, tags: [] });
|
||||||
|
paperless.getTask
|
||||||
|
.mockResolvedValueOnce({ status: 'STARTED' })
|
||||||
|
.mockResolvedValueOnce({ status: 'SUCCESS' });
|
||||||
|
|
||||||
|
await service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123');
|
||||||
|
|
||||||
|
expect(paperless.getTask).toHaveBeenCalledWith('task-1');
|
||||||
|
expect(paperless.getTask).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet einen Fehler und entfernt Tag 21, wenn Paperless die Version ablehnt', async () => {
|
||||||
|
paperless.getDocument
|
||||||
|
.mockResolvedValueOnce({ id: 42, tags: [3] })
|
||||||
|
.mockResolvedValueOnce({ id: 42, tags: [3, 21] });
|
||||||
|
paperless.getTask.mockResolvedValue({
|
||||||
|
status: 'FAILURE',
|
||||||
|
message: 'kaputt',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123'),
|
||||||
|
).rejects.toThrow(/kaputt/);
|
||||||
|
expect(paperless.updateDocument).toHaveBeenLastCalledWith(42, {
|
||||||
|
tags: [3],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setzt Tag 21 vor dem Hochladen der Version', async () => {
|
it('setzt Tag 21 vor dem Hochladen der Version', async () => {
|
||||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [3, 17] });
|
paperless.getDocument.mockResolvedValue({ id: 42, tags: [3, 17] });
|
||||||
|
|
||||||
await service.haengeVersionAn(42, '/tmp/x.pdf');
|
await service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123');
|
||||||
|
|
||||||
expect(paperless.updateDocument).toHaveBeenCalledWith(42, {
|
expect(paperless.updateDocument).toHaveBeenCalledWith(42, {
|
||||||
tags: [3, 17, 21],
|
tags: [3, 17, 21],
|
||||||
@@ -52,7 +106,7 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
it('setzt Tag 21 nicht doppelt', async () => {
|
it('setzt Tag 21 nicht doppelt', async () => {
|
||||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21, 5] });
|
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21, 5] });
|
||||||
|
|
||||||
await service.haengeVersionAn(42, '/tmp/x.pdf');
|
await service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123');
|
||||||
|
|
||||||
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
||||||
expect(paperless.uploadDocumentVersion).toHaveBeenCalled();
|
expect(paperless.uploadDocumentVersion).toHaveBeenCalled();
|
||||||
@@ -65,9 +119,9 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
.mockResolvedValueOnce({ id: 42, tags: [3, 21] });
|
.mockResolvedValueOnce({ id: 42, tags: [3, 21] });
|
||||||
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
||||||
|
|
||||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
await expect(
|
||||||
fehler,
|
service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123'),
|
||||||
);
|
).rejects.toBe(fehler);
|
||||||
expect(paperless.updateDocument).toHaveBeenLastCalledWith(42, {
|
expect(paperless.updateDocument).toHaveBeenLastCalledWith(42, {
|
||||||
tags: [3],
|
tags: [3],
|
||||||
});
|
});
|
||||||
@@ -78,9 +132,9 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21] });
|
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21] });
|
||||||
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
||||||
|
|
||||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
await expect(
|
||||||
fehler,
|
service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123'),
|
||||||
);
|
).rejects.toBe(fehler);
|
||||||
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,8 +146,8 @@ describe('DokumentVersionService.haengeVersionAn', () => {
|
|||||||
.mockResolvedValueOnce({})
|
.mockResolvedValueOnce({})
|
||||||
.mockRejectedValueOnce(new Error('Tag entfernen kaputt'));
|
.mockRejectedValueOnce(new Error('Tag entfernen kaputt'));
|
||||||
|
|
||||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
await expect(
|
||||||
fehler,
|
service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123'),
|
||||||
);
|
).rejects.toBe(fehler);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { ConflictException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { PaperlessService } from './paperless.service';
|
import { PaperlessService } from './paperless.service';
|
||||||
|
|
||||||
/** Tag „neue Version hochgeladen“ – Postprocessing-Regeln ignorieren solche Belege. */
|
/** Tag „neue Version hochgeladen“ – Postprocessing-Regeln ignorieren solche Belege. */
|
||||||
@@ -6,6 +6,10 @@ export const NEUE_VERSION_TAG_ID = 21;
|
|||||||
|
|
||||||
const pad = (n: number) => String(n).padStart(2, '0');
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
/** Wie beim normalen Import: bis zu 30 × 2 s auf die Paperless-Verarbeitung warten. */
|
||||||
|
const MAX_ABFRAGEN = 30;
|
||||||
|
const ABFRAGE_ABSTAND_MS = 2000;
|
||||||
|
|
||||||
/** Bezeichnung der Version in Paperless, z. B. „Korrektur vom 02.11.2026“. */
|
/** Bezeichnung der Version in Paperless, z. B. „Korrektur vom 02.11.2026“. */
|
||||||
export function versionLabel(datum: Date): string {
|
export function versionLabel(datum: Date): string {
|
||||||
return `Korrektur vom ${pad(datum.getDate())}.${pad(datum.getMonth() + 1)}.${datum.getFullYear()}`;
|
return `Korrektur vom ${pad(datum.getDate())}.${pad(datum.getMonth() + 1)}.${datum.getFullYear()}`;
|
||||||
@@ -18,21 +22,35 @@ export class DokumentVersionService {
|
|||||||
constructor(private readonly paperlessService: PaperlessService) {}
|
constructor(private readonly paperlessService: PaperlessService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hängt eine PDF als neue Version an ein Stammdokument. Erst wird Tag 21
|
* Hängt eine PDF als neue Version an ein Stammdokument. Vorher wird geprüft,
|
||||||
* gesetzt, dann die Version hochgeladen; scheitert der Upload, wird ein
|
* dass die Belegnummer in Paperless eindeutig zu genau diesem Dokument
|
||||||
* hier neu gesetzter Tag 21 wieder entfernt.
|
* gehört. Erst wird Tag 21 gesetzt, dann die Version hochgeladen und auf die
|
||||||
|
* Verarbeitung gewartet; scheitert das, wird ein hier neu gesetzter Tag 21
|
||||||
|
* wieder entfernt.
|
||||||
*/
|
*/
|
||||||
async haengeVersionAn(
|
async haengeVersionAn(
|
||||||
rootDocumentId: number,
|
rootDocumentId: number,
|
||||||
pdfPath: string,
|
pdfPath: string,
|
||||||
|
belegnummer: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const pruefung = await this.paperlessService.pruefeBelegnummer(belegnummer);
|
||||||
|
if (
|
||||||
|
pruefung.status !== 'eindeutig' ||
|
||||||
|
pruefung.documentId !== rootDocumentId
|
||||||
|
) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Belegnummer ${belegnummer} gehört in Paperless nicht eindeutig zu Dokument ${rootDocumentId} (gefunden: ${pruefung.kandidaten.join(', ') || 'keins'}) – keine neue Version angelegt.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const tagNeuGesetzt = await this.setzeTag(rootDocumentId);
|
const tagNeuGesetzt = await this.setzeTag(rootDocumentId);
|
||||||
try {
|
try {
|
||||||
await this.paperlessService.uploadDocumentVersion(
|
const taskId = await this.paperlessService.uploadDocumentVersion(
|
||||||
rootDocumentId,
|
rootDocumentId,
|
||||||
pdfPath,
|
pdfPath,
|
||||||
versionLabel(new Date()),
|
versionLabel(new Date()),
|
||||||
);
|
);
|
||||||
|
await this.warteAufVerarbeitung(rootDocumentId, taskId);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Neue Version für Dokument ${rootDocumentId} hochgeladen.`,
|
`Neue Version für Dokument ${rootDocumentId} hochgeladen.`,
|
||||||
);
|
);
|
||||||
@@ -42,6 +60,41 @@ export class DokumentVersionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wartet, bis Paperless die Version verarbeitet hat. FAILURE/REVOKED → Fehler;
|
||||||
|
* bleibt die Task offen, wird nur gewarnt (Ergebnis unbekannt).
|
||||||
|
*/
|
||||||
|
private async warteAufVerarbeitung(
|
||||||
|
documentId: number,
|
||||||
|
taskId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!taskId) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Keine Task-ID für die Version von Dokument ${documentId} – Verarbeitung nicht überprüfbar.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < MAX_ABFRAGEN; i++) {
|
||||||
|
const task = await this.paperlessService
|
||||||
|
.getTask(taskId)
|
||||||
|
.catch(() => null);
|
||||||
|
if (task?.status === 'SUCCESS') return;
|
||||||
|
if (task?.status === 'FAILURE' || task?.status === 'REVOKED') {
|
||||||
|
throw new Error(
|
||||||
|
`Paperless hat die neue Version für Dokument ${documentId} abgelehnt: ${task.message ?? 'keine Meldung'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.warte(ABFRAGE_ABSTAND_MS);
|
||||||
|
}
|
||||||
|
this.logger.warn(
|
||||||
|
`Version für Dokument ${documentId} (Task ${taskId}) nach ${(MAX_ABFRAGEN * ABFRAGE_ABSTAND_MS) / 1000} s noch nicht verarbeitet.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected warte(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
/** Setzt Tag 21; liefert true, wenn er vorher fehlte. */
|
/** Setzt Tag 21; liefert true, wenn er vorher fehlte. */
|
||||||
private async setzeTag(documentId: number): Promise<boolean> {
|
private async setzeTag(documentId: number): Promise<boolean> {
|
||||||
const tags = await this.leseTags(documentId);
|
const tags = await this.leseTags(documentId);
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PaperlessService } from './paperless.service';
|
||||||
|
|
||||||
|
describe('PaperlessService – Belegnummer-Suche', () => {
|
||||||
|
let service: PaperlessService;
|
||||||
|
let get: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new PaperlessService({
|
||||||
|
get: (_k: string, d?: unknown) => d,
|
||||||
|
} as unknown as ConfigService);
|
||||||
|
get = jest.fn();
|
||||||
|
(service as unknown as { client: { get: jest.Mock } }).client = { get };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('führt Stamm und Version auf das Stammdokument zusammen (CF7)', async () => {
|
||||||
|
get.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 42,
|
||||||
|
root_document: null,
|
||||||
|
custom_fields: [{ field: 7, value: '2026-000123' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 99,
|
||||||
|
root_document: 42,
|
||||||
|
custom_fields: [{ field: 7, value: '2026-000123' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 50,
|
||||||
|
root_document: null,
|
||||||
|
custom_fields: [{ field: 7, value: '2026-0001234' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await service.findRootDocumentIdsByCustomField(7, '2026-000123'),
|
||||||
|
).toEqual([42, 42]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('führt Stamm und Version auf das Stammdokument zusammen (ASN)', async () => {
|
||||||
|
get.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{ id: 42, archive_serial_number: 2026000123 },
|
||||||
|
{ id: 99, root_document: 42, archive_serial_number: 2026000123 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await service.findRootDocumentIdsByAsn(2026000123)).toEqual([
|
||||||
|
42, 42,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ein Dokument mit Versionen gilt als eindeutig', async () => {
|
||||||
|
get.mockImplementation(
|
||||||
|
(_url: string, cfg: { params: Record<string, unknown> }) =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
results: cfg.params.archive_serial_number
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 99,
|
||||||
|
root_document: 42,
|
||||||
|
archive_serial_number: 2026000123,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: 42,
|
||||||
|
custom_fields: [{ field: 7, value: '2026-000123' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await service.pruefeBelegnummer('2026-000123')).toEqual({
|
||||||
|
status: 'eindeutig',
|
||||||
|
documentId: 42,
|
||||||
|
kandidaten: [42],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -148,6 +148,8 @@ export interface PostprocessActionResult {
|
|||||||
duplikatKandidaten?: number[];
|
duplikatKandidaten?: number[];
|
||||||
verworfen?: boolean;
|
verworfen?: boolean;
|
||||||
belegnummer?: string;
|
belegnummer?: string;
|
||||||
|
/** Original-Seiten des Abschnitts laut Backend (bei Duplikaten) */
|
||||||
|
seiten?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clientsApi = {
|
export const clientsApi = {
|
||||||
|
|||||||
@@ -422,7 +422,8 @@ function PostprocessWizardModal({
|
|||||||
{compareForSection !== null && (() => {
|
{compareForSection !== null && (() => {
|
||||||
const dup = steps[compareForSection]?.results?.find((r) => r.skipped);
|
const dup = steps[compareForSection]?.results?.find((r) => r.skipped);
|
||||||
const docId = dup?.duplicateOfDocumentId ?? null;
|
const docId = dup?.duplicateOfDocumentId ?? null;
|
||||||
const pages = documents[compareForSection]?.pages ?? [];
|
// Seiten so, wie das Backend den Abschnitt hochlädt (manuelle Splits zählen dort nicht)
|
||||||
|
const pages = dup?.seiten ?? documents[compareForSection]?.pages ?? [];
|
||||||
const kandidaten = dup?.duplikatKandidaten ?? [];
|
const kandidaten = dup?.duplikatKandidaten ?? [];
|
||||||
const pruefung: BelegnummerPruefung | null = kandidaten.length
|
const pruefung: BelegnummerPruefung | null = kandidaten.length
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in new issue
Block a user