feat(mail): Beleg beim Import als neue Version anhängen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-11-02 14:01:47 +01:00
1 parent bdc85f5f1f
commit 21a447e915
3 files changed
+120 -9

No files matched your search

@@ -14,6 +14,7 @@ import { PdfService } from '../preprocessing/pdf.service';
import { EmailPageCacheService } from './email-page-cache.service';
import { ImapFolderService } from './imap-folder.service';
import { AttachmentFingerprintService } from './attachment-fingerprint.service';
import { DokumentVersionService } from '../paperless/dokument-version.service';
async function pdfMitSeiten(anzahl: number): Promise<Buffer> {
const doc = await PDFDocument.create();
@@ -29,11 +30,19 @@ describe('EmailImportService – Duplikatprüfung', () => {
andWhere: jest.Mock;
getCount: jest.Mock;
};
let attachmentRepo: { findOne: jest.Mock };
let attachmentRepo: { findOne: jest.Mock; save: jest.Mock };
let taskRepo: {
createQueryBuilder: jest.Mock;
create: jest.Mock;
save: jest.Mock;
};
let emailRepo: { update: jest.Mock; findOne: jest.Mock };
let dokumentVersion: { haengeVersionAn: jest.Mock };
let contentRepo: { findOne: jest.Mock };
let paperlessService: {
checksumExists: jest.Mock;
uploadDocument: jest.Mock;
validateAsnNotExists: jest.Mock;
};
let fingerprints: { ensure: jest.Mock; hasImportedTwin: jest.Mock };
@@ -50,11 +59,25 @@ describe('EmailImportService – Duplikatprüfung', () => {
Checksum: 'abc123',
FileName: 'rechnung.pdf',
}),
save: jest.fn().mockImplementation((a: unknown) => Promise.resolve(a)),
};
taskRepo = {
createQueryBuilder: jest.fn().mockReturnValue(taskQuery),
create: jest.fn().mockImplementation((t: unknown) => t),
save: jest.fn(),
};
emailRepo = {
update: jest.fn().mockResolvedValue({}),
findOne: jest.fn().mockResolvedValue(null),
};
dokumentVersion = {
haengeVersionAn: jest.fn().mockResolvedValue(undefined),
};
contentRepo = { findOne: jest.fn() };
paperlessService = {
checksumExists: jest.fn().mockResolvedValue(false),
uploadDocument: jest.fn(),
validateAsnNotExists: jest.fn(),
};
fingerprints = {
ensure: jest.fn().mockResolvedValue(null),
@@ -65,24 +88,20 @@ describe('EmailImportService – Duplikatprüfung', () => {
providers: [
EmailImportService,
{ provide: ConfigService, useValue: { get: jest.fn() } },
{ provide: getRepositoryToken(Email), useValue: {} },
{ provide: getRepositoryToken(Email), useValue: emailRepo },
{ provide: getRepositoryToken(Attachment), useValue: attachmentRepo },
{ provide: getRepositoryToken(Content), useValue: contentRepo },
{
provide: getRepositoryToken(CorrespondentEmailMapping),
useValue: {},
},
{
provide: getRepositoryToken(Task),
useValue: {
createQueryBuilder: jest.fn().mockReturnValue(taskQuery),
},
},
{ provide: getRepositoryToken(Task), useValue: taskRepo },
{ provide: PaperlessService, useValue: paperlessService },
{ provide: PdfService, useValue: {} },
{ provide: EmailPageCacheService, useValue: {} },
{ provide: ImapFolderService, useValue: {} },
{ provide: AttachmentFingerprintService, useValue: fingerprints },
{ provide: DokumentVersionService, useValue: dokumentVersion },
],
}).compile();
@@ -189,6 +208,48 @@ describe('EmailImportService – Duplikatprüfung', () => {
});
});
describe('Import als Version', () => {
it('hängt den Beleg als Version an, statt ein neues Dokument anzulegen', async () => {
contentRepo.findOne.mockResolvedValue({
Content1: await pdfMitSeiten(1),
});
await service.executeImport({
emailDate: '2026-09-25',
attachments: [
{
attachmentId: 7,
type: 'MAIN',
belegnummer: '2026-000123',
barcode: {
x: 0,
y: 0,
nummer: '000123',
datum: '2026-09-25',
jahr: '2026',
},
versionZuDokumentId: 42,
},
],
});
expect(dokumentVersion.haengeVersionAn).toHaveBeenCalledTimes(1);
expect(dokumentVersion.haengeVersionAn).toHaveBeenCalledWith(
42,
expect.stringMatching(/\.pdf$/),
);
expect(paperlessService.uploadDocument).not.toHaveBeenCalled();
expect(paperlessService.validateAsnNotExists).not.toHaveBeenCalled();
expect(taskRepo.save).not.toHaveBeenCalled();
expect(attachmentRepo.save).toHaveBeenCalledWith(
expect.objectContaining({
PaperlessDocumentIds: { full: 42 },
ImportStatus: 1,
}),
);
});
});
describe('executeImport', () => {
const importDaten = (splitRanges?: { start: number; end: number }[]) => ({
emailDate: '2026-09-25',
@@ -21,6 +21,7 @@ import * as QRCode from 'qrcode';
import { EmailPageCacheService } from './email-page-cache.service';
import { ImapFolderService } from './imap-folder.service';
import { AttachmentFingerprintService } from './attachment-fingerprint.service';
import { DokumentVersionService } from '../paperless/dokument-version.service';
import { PdfService } from '../preprocessing/pdf.service';
import * as path from 'path';
import * as os from 'os';
@@ -73,6 +74,7 @@ export class EmailImportService {
private readonly pageCache: EmailPageCacheService,
private readonly imapFolderService: ImapFolderService,
private readonly fingerprints: AttachmentFingerprintService,
private readonly dokumentVersion: DokumentVersionService,
) {}
async ensurePreviews(emailId: number): Promise<void> {
@@ -568,6 +570,8 @@ export class EmailImportService {
jahr: string;
};
belegnummer?: string;
/** Stammdokument in Paperless, an das der Beleg als neue Version kommt */
versionZuDokumentId?: number | null;
}[];
emailDate: string;
}): Promise<{ success: boolean; results: any[] }> {
@@ -634,6 +638,26 @@ export class EmailImportService {
}
}
/**
* Hängt jede Upload-Datei als neue Version an das Stammdokument. Kein Task,
* keine Belegnummer-Bestätigung – Metadaten des Dokuments bleiben unverändert.
*/
private async importiereAlsVersion(
documentId: number,
uploadItems: { path: string; filename: string; rangeKey: string }[],
paperlessIds: Record<string, number>,
jobId?: string,
): Promise<void> {
for (const item of uploadItems) {
this.setJobStatus(
jobId,
`Hänge ${item.filename} als neue Version an Dokument ${documentId}...`,
);
await this.dokumentVersion.haengeVersionAn(documentId, item.path);
paperlessIds[item.rangeKey] = documentId;
}
}
private async runImport(data: {
jobId?: string;
attachments: {
@@ -650,6 +674,8 @@ export class EmailImportService {
jahr: string;
};
belegnummer?: string;
/** Stammdokument in Paperless, an das der Beleg als neue Version kommt */
versionZuDokumentId?: number | null;
}[];
emailDate: string;
}): Promise<{ success: boolean; results: any[] }> {
@@ -677,7 +703,7 @@ export class EmailImportService {
const baseFilename = sanitizeFilename(
attachmentEntity.FileName.replace(/\.pdf$/i, ''),
);
const paperlessIds: any = {};
const paperlessIds: Record<string, number> = {};
const uploadPromises = [];
@@ -735,6 +761,28 @@ export class EmailImportService {
});
}
// Vom Benutzer bestätigt: als neue Version an das vorhandene Dokument
if (att.versionZuDokumentId) {
await this.importiereAlsVersion(
att.versionZuDokumentId,
uploadPromises,
paperlessIds,
data.jobId,
);
attachmentEntity.PaperlessDocumentIds = paperlessIds;
attachmentEntity.ImportStatus = 1;
if (att.belegnummer) {
attachmentEntity.InterneBelegnummer = att.belegnummer;
}
await this.attachmentRepo.save(attachmentEntity);
results.push({
attachmentId: att.attachmentId,
paperlessIds,
alsVersion: true,
});
continue;
}
// 0. Check if ASN already exists
if (att.belegnummer) {
await this.paperlessService.validateAsnNotExists(att.belegnummer);
@@ -20,6 +20,8 @@ export interface AttachmentImportData {
barcode?: { x: number; y: number; nummer: string; datum: string; jahr: string };
belegnummer?: string;
isDuplicate?: boolean;
/** Stammdokument in Paperless, an das der Beleg als neue Version kommt */
versionZuDokumentId?: number | null;
}
export const emailImportApi = {