feat: Duplikate im Import als neue Version behandeln #6

Merged
bjoernpoettker merged 12 commits from feat/duplikat-neue-version into main 2026-11-02 13:31:04 +00:00
3 changed files with 70 additions and 6 deletions
Showing only changes of commit 79dadb9c93 - Show all commits

No files matched your search

@@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConflictException } from '@nestjs/common';
import { BadRequestException, ConflictException } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { PDFDocument } from 'pdf-lib';
@@ -208,6 +208,33 @@ describe('EmailImportService – Duplikatprüfung', () => {
});
});
describe('getAttachmentPdf', () => {
beforeEach(async () => {
contentRepo.findOne.mockResolvedValue({
Content1: await pdfMitSeiten(3),
});
});
it('liefert den gewünschten Seitenbereich', async () => {
const pdf = await service.getAttachmentPdf(7, { start: 2, end: 3 });
expect((await PDFDocument.load(pdf)).getPageCount()).toBe(2);
});
it.each([
{ start: 3, end: 2 },
{ start: 0, end: 2 },
{ start: 5, end: 6 },
{ start: Number.NaN, end: 2 },
])(
'meldet einen ungültigen Seitenbereich $start–$end als 400',
async (pages) => {
await expect(service.getAttachmentPdf(7, pages)).rejects.toThrow(
BadRequestException,
);
},
);
});
describe('Import als Version', () => {
it('hängt den Beleg als Version an, statt ein neues Dokument anzulegen', async () => {
contentRepo.findOne.mockResolvedValue({
@@ -4,6 +4,7 @@ import {
HttpException,
HttpStatus,
ConflictException,
BadRequestException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
@@ -370,8 +371,19 @@ export class EmailImportService {
ignoreEncryption: true,
});
const total = pdfDoc.getPageCount();
const startIdx = Math.max(1, pages.start) - 1;
const endIdx = Math.min(pages.end === 999 ? total : pages.end, total) - 1;
const ende = pages.end === 999 ? total : Math.min(pages.end, total);
if (
!Number.isInteger(pages.start) ||
!Number.isInteger(ende) ||
pages.start < 1 ||
pages.start > ende
) {
throw new BadRequestException(
`Ungültiger Seitenbereich ${pages.start}–${pages.end} (Dokument hat ${total} Seiten)`,
);
}
const startIdx = pages.start - 1;
const endIdx = ende - 1;
const sliced = await PDFDocument.create();
const indices = Array.from(
{ length: endIdx - startIdx + 1 },
@@ -3,6 +3,25 @@ import { Alert, Button, Modal, Space, Spin, Tag, Typography, message } from 'ant
import { paperlessApi, type BelegnummerPruefung, type VergleichKennzahlen } from '../api/paperless';
import { useIsMobile } from '../hooks/useIsMobile';
/** Länger wird auf Seiten-/Wortzahl nicht gewartet – der Vergleich bleibt trotzdem möglich. */
const KENNZAHLEN_TIMEOUT_MS = 30000;
function mitTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Zeitüberschreitung')), ms);
promise.then(
(wert) => {
clearTimeout(timer);
resolve(wert);
},
(err: unknown) => {
clearTimeout(timer);
reject(err instanceof Error ? err : new Error(String(err)));
},
);
});
}
export interface DuplikatVergleichModalProps {
open: boolean;
belegnummer: string;
@@ -70,6 +89,7 @@ export function DuplikatVergleichModal({
const [vorhandenUrl, setVorhandenUrl] = useState<string | null>(null);
const [neuUrl, setNeuUrl] = useState<string | null>(null);
const [kennzahlen, setKennzahlen] = useState<VergleichKennzahlen | null>(null);
const [kennzahlenFehler, setKennzahlenFehler] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
@@ -79,6 +99,7 @@ export function DuplikatVergleichModal({
let urlNeu: string | null = null;
setLoading(true);
setKennzahlen(null);
setKennzahlenFehler(false);
(async () => {
try {
const [altBlob, neuBlob] = await Promise.all([paperlessApi.getDocumentPdfBlob(paperlessDocumentId), ladeNeuesPdf()]);
@@ -93,12 +114,12 @@ export function DuplikatVergleichModal({
if (!cancelled) setLoading(false);
}
})();
ladeKennzahlen()
mitTimeout(ladeKennzahlen(), KENNZAHLEN_TIMEOUT_MS)
.then((k) => {
if (!cancelled) setKennzahlen(k);
})
.catch(() => {
if (!cancelled) message.warning('Seiten- und Wortzahl konnten nicht ermittelt werden');
if (!cancelled) setKennzahlenFehler(true);
});
return () => {
cancelled = true;
@@ -114,7 +135,11 @@ export function DuplikatVergleichModal({
const versionMoeglich = pruefung?.status === 'eindeutig';
let auffaelligkeiten: ReactNode = <Spin size="small" />;
if (kennzahlen) {
if (kennzahlenFehler) {
auffaelligkeiten = (
<Alert type="warning" showIcon message="Seiten- und Wortzahl konnten nicht ermittelt werden – bitte die PDFs direkt vergleichen." />
);
} else if (kennzahlen) {
const { vorhanden, neu } = kennzahlen;
const alleBekannt = [vorhanden.seiten, neu.seiten, vorhanden.woerter, neu.woerter].every((w) => w !== null);
const gleich = alleBekannt && vorhanden.seiten === neu.seiten && vorhanden.woerter === neu.woerter;