feat(paperless): Belege als neue Version anhängen und mit Tag 21 markieren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8e0c609802
commit
170eb72c70
4 files changed
+208
-1
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
DokumentVersionService,
|
||||
versionLabel,
|
||||
} from './dokument-version.service';
|
||||
import { PaperlessService } from './paperless.service';
|
||||
|
||||
describe('versionLabel', () => {
|
||||
it('benennt die Version nach dem Datum', () => {
|
||||
expect(versionLabel(new Date(2026, 10, 2))).toBe(
|
||||
'Korrektur vom 02.11.2026',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DokumentVersionService.haengeVersionAn', () => {
|
||||
let paperless: {
|
||||
getDocument: jest.Mock;
|
||||
updateDocument: jest.Mock;
|
||||
uploadDocumentVersion: jest.Mock;
|
||||
};
|
||||
let service: DokumentVersionService;
|
||||
|
||||
beforeEach(() => {
|
||||
paperless = {
|
||||
getDocument: jest.fn(),
|
||||
updateDocument: jest.fn().mockResolvedValue({}),
|
||||
uploadDocumentVersion: jest.fn().mockResolvedValue('task-1'),
|
||||
};
|
||||
service = new DokumentVersionService(
|
||||
paperless as unknown as PaperlessService,
|
||||
);
|
||||
});
|
||||
|
||||
it('setzt Tag 21 vor dem Hochladen der Version', async () => {
|
||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [3, 17] });
|
||||
|
||||
await service.haengeVersionAn(42, '/tmp/x.pdf');
|
||||
|
||||
expect(paperless.updateDocument).toHaveBeenCalledWith(42, {
|
||||
tags: [3, 17, 21],
|
||||
});
|
||||
expect(paperless.uploadDocumentVersion).toHaveBeenCalledWith(
|
||||
42,
|
||||
'/tmp/x.pdf',
|
||||
expect.stringMatching(/^Korrektur vom \d{2}\.\d{2}\.\d{4}$/),
|
||||
);
|
||||
expect(paperless.updateDocument.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
paperless.uploadDocumentVersion.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('setzt Tag 21 nicht doppelt', async () => {
|
||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21, 5] });
|
||||
|
||||
await service.haengeVersionAn(42, '/tmp/x.pdf');
|
||||
|
||||
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
||||
expect(paperless.uploadDocumentVersion).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('entfernt den neu gesetzten Tag, wenn der Upload scheitert', async () => {
|
||||
const fehler = new Error('Upload kaputt');
|
||||
paperless.getDocument
|
||||
.mockResolvedValueOnce({ id: 42, tags: [3] })
|
||||
.mockResolvedValueOnce({ id: 42, tags: [3, 21] });
|
||||
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
||||
|
||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
||||
fehler,
|
||||
);
|
||||
expect(paperless.updateDocument).toHaveBeenLastCalledWith(42, {
|
||||
tags: [3],
|
||||
});
|
||||
});
|
||||
|
||||
it('lässt einen schon vorhandenen Tag 21 stehen, wenn der Upload scheitert', async () => {
|
||||
const fehler = new Error('Upload kaputt');
|
||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [21] });
|
||||
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
||||
|
||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
||||
fehler,
|
||||
);
|
||||
expect(paperless.updateDocument).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('meldet den Upload-Fehler, auch wenn das Entfernen des Tags scheitert', async () => {
|
||||
const fehler = new Error('Upload kaputt');
|
||||
paperless.getDocument.mockResolvedValue({ id: 42, tags: [3] });
|
||||
paperless.uploadDocumentVersion.mockRejectedValue(fehler);
|
||||
paperless.updateDocument
|
||||
.mockResolvedValueOnce({})
|
||||
.mockRejectedValueOnce(new Error('Tag entfernen kaputt'));
|
||||
|
||||
await expect(service.haengeVersionAn(42, '/tmp/x.pdf')).rejects.toBe(
|
||||
fehler,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PaperlessService } from './paperless.service';
|
||||
|
||||
/** Tag „neue Version hochgeladen“ – Postprocessing-Regeln ignorieren solche Belege. */
|
||||
export const NEUE_VERSION_TAG_ID = 21;
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
/** Bezeichnung der Version in Paperless, z. B. „Korrektur vom 02.11.2026“. */
|
||||
export function versionLabel(datum: Date): string {
|
||||
return `Korrektur vom ${pad(datum.getDate())}.${pad(datum.getMonth() + 1)}.${datum.getFullYear()}`;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DokumentVersionService {
|
||||
private readonly logger = new Logger(DokumentVersionService.name);
|
||||
|
||||
constructor(private readonly paperlessService: PaperlessService) {}
|
||||
|
||||
/**
|
||||
* Hängt eine PDF als neue Version an ein Stammdokument. Erst wird Tag 21
|
||||
* gesetzt, dann die Version hochgeladen; scheitert der Upload, wird ein
|
||||
* hier neu gesetzter Tag 21 wieder entfernt.
|
||||
*/
|
||||
async haengeVersionAn(
|
||||
rootDocumentId: number,
|
||||
pdfPath: string,
|
||||
): Promise<void> {
|
||||
const tagNeuGesetzt = await this.setzeTag(rootDocumentId);
|
||||
try {
|
||||
await this.paperlessService.uploadDocumentVersion(
|
||||
rootDocumentId,
|
||||
pdfPath,
|
||||
versionLabel(new Date()),
|
||||
);
|
||||
this.logger.log(
|
||||
`Neue Version für Dokument ${rootDocumentId} hochgeladen.`,
|
||||
);
|
||||
} catch (err) {
|
||||
if (tagNeuGesetzt) await this.entferneTag(rootDocumentId);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** Setzt Tag 21; liefert true, wenn er vorher fehlte. */
|
||||
private async setzeTag(documentId: number): Promise<boolean> {
|
||||
const tags = await this.leseTags(documentId);
|
||||
if (tags.includes(NEUE_VERSION_TAG_ID)) return false;
|
||||
await this.paperlessService.updateDocument(documentId, {
|
||||
tags: [...tags, NEUE_VERSION_TAG_ID],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private async entferneTag(documentId: number): Promise<void> {
|
||||
try {
|
||||
const tags = await this.leseTags(documentId);
|
||||
await this.paperlessService.updateDocument(documentId, {
|
||||
tags: tags.filter((t) => t !== NEUE_VERSION_TAG_ID),
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Tag ${NEUE_VERSION_TAG_ID} konnte nach fehlgeschlagenem Versions-Upload nicht von Dokument ${documentId} entfernt werden: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async leseTags(documentId: number): Promise<number[]> {
|
||||
const doc = (await this.paperlessService.getDocument(documentId)) as {
|
||||
tags?: number[];
|
||||
};
|
||||
return Array.isArray(doc?.tags) ? doc.tags.map(Number) : [];
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { PaperlessService } from './paperless.service';
|
||||
import { PaperlessController } from './paperless.controller';
|
||||
import { PaperlessProcessorService } from './paperless-processor.service';
|
||||
import { PaperlessTaskProcessorService } from './paperless-task-processor.service';
|
||||
import { DokumentVersionService } from './dokument-version.service';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { Task } from '../database/entities/task.entity';
|
||||
@@ -33,7 +34,12 @@ import { ErechnungModule } from '../erechnung/erechnung.module';
|
||||
PaperlessService,
|
||||
PaperlessProcessorService,
|
||||
PaperlessTaskProcessorService,
|
||||
DokumentVersionService,
|
||||
],
|
||||
exports: [
|
||||
PaperlessService,
|
||||
PaperlessProcessorService,
|
||||
DokumentVersionService,
|
||||
],
|
||||
exports: [PaperlessService, PaperlessProcessorService],
|
||||
})
|
||||
export class PaperlessModule {}
|
||||
@@ -146,6 +146,34 @@ export class PaperlessService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt eine PDF als neue Version eines vorhandenen Dokuments hoch
|
||||
* (`update_version`, Paperless-NGX API v10). Liefert die Consume-Task-ID.
|
||||
*/
|
||||
async uploadDocumentVersion(
|
||||
documentId: number,
|
||||
filePath: string,
|
||||
versionLabel: string,
|
||||
): Promise<string> {
|
||||
const form = new FormData();
|
||||
form.append('document', fs.createReadStream(filePath), {
|
||||
filename: path.basename(filePath),
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
form.append('version_label', versionLabel.slice(0, 64));
|
||||
|
||||
const response = await this.client.post<string>(
|
||||
`/documents/${documentId}/update_version/`,
|
||||
form,
|
||||
{ headers: form.getHeaders(), timeout: 120000 },
|
||||
);
|
||||
const taskId = (response.data ?? '').replace(/"/g, '');
|
||||
this.logger.log(
|
||||
`Version für Dokument ${documentId} hochgeladen: ${taskId || '(keine Task-ID)'}`,
|
||||
);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async getUsers(): Promise<any[]> {
|
||||
const response = await this.client.get('/users/');
|
||||
return response.data?.results ?? [];
|
||||
|
||||
Reference in new issue
Block a user