feat(backend): Metadaten direkt beim Paperless-Upload übergeben
Build and Push Multi-Platform Images / build-and-push (push) Successful in 31s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 31s
E-Mail-Import und External-Upload setzen ASN, Custom Fields (CF3/CF7/CF9), Dokumenttyp, Tags, Owner und Belegdatum jetzt bereits beim post_document, statt erst nachträglich über den Task-Processor-Cron (30 s + PATCH). - Neu: deriveTaskMetadata() in task-metadata.util.ts als gemeinsame Ableitung (Task-Felder → Paperless-Metadaten) für beide Upload-Pfade und den Task-Processor – identische Parsing-Regeln, kein Copy-Paste - Task-Processor nutzt den Helper (verhaltensidentischer Refactor) und patcht weiterhin idempotent nach; zuständig bleibt er für alles ID-Abhängige (CF8-Verknüpfungen, Anlage-Titel, Owner-Entfernung, Notizen) - FAILURE-Zweig loggt Paperless-Fehlertext und Belegnummer vor dem Löschen des Tasks (ASN-Kollision kann jetzt den Consume scheitern lassen) - Tests: 18 Unit-Tests für deriveTaskMetadata, 3 Charakterisierungstests pinnen den PATCH-Payload von processSuccessfulTask Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { PaperlessTaskProcessorService } from './paperless-task-processor.service';
|
||||
import { Task } from '../database/entities/task.entity';
|
||||
|
||||
/**
|
||||
* Charakterisierungstest für processSuccessfulTask: pinnt den PATCH-Payload
|
||||
* fest, damit der Refactor auf deriveTaskMetadata() verhaltensidentisch bleibt.
|
||||
* custom_fields wird ordnungsunabhängig verglichen (Paperless behandelt das
|
||||
* Array als Menge).
|
||||
*/
|
||||
|
||||
function createTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
TaskId: 'task-uuid-1',
|
||||
InterneBelegnummer: '',
|
||||
DocumentType: null,
|
||||
Eingangsdatum: null,
|
||||
Fertig: 0,
|
||||
Tags: null,
|
||||
BetriebID: null,
|
||||
Lieferant: null,
|
||||
externeBelegnummer: null,
|
||||
EinkaufID: null,
|
||||
Belegdatum: null,
|
||||
PaperlessDocumentID: null,
|
||||
TaskReferenceID: null,
|
||||
BarcodeJson: null,
|
||||
DuplikatZU: null,
|
||||
CustomFieldsJson: null,
|
||||
Asn: null,
|
||||
SourceAttachmentID: null,
|
||||
SourceAttachmentRange: null,
|
||||
...overrides,
|
||||
} as Task;
|
||||
}
|
||||
|
||||
function sortByField(fields: Array<{ field: number }>) {
|
||||
return [...fields].sort((a, b) => a.field - b.field);
|
||||
}
|
||||
|
||||
/** Erwartete Form des PATCH-Payloads an updateDocument. */
|
||||
interface PatchPayload {
|
||||
custom_fields: Array<{ field: number; value: unknown }>;
|
||||
archive_serial_number?: number;
|
||||
document_type?: number;
|
||||
created?: string;
|
||||
owner?: number | null;
|
||||
tags?: number[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
describe('PaperlessTaskProcessorService.processSuccessfulTask', () => {
|
||||
let paperlessService: {
|
||||
getDocument: jest.Mock;
|
||||
updateDocument: jest.Mock;
|
||||
addNote: jest.Mock;
|
||||
getDocumentMetadata: jest.Mock;
|
||||
getTask: jest.Mock;
|
||||
};
|
||||
let taskRepo: { save: jest.Mock };
|
||||
let documentRepo: { findOne: jest.Mock; create: jest.Mock; save: jest.Mock };
|
||||
let attachmentRepo: { findOne: jest.Mock; save: jest.Mock };
|
||||
let service: PaperlessTaskProcessorService;
|
||||
|
||||
/** Zugriff auf die private Methode, typsicher für den Test gekapselt. */
|
||||
function runProcessSuccessfulTask(
|
||||
task: Task,
|
||||
apiTask: { related_document: number },
|
||||
parentTask: Task | null,
|
||||
): Promise<void> {
|
||||
return (
|
||||
service as unknown as {
|
||||
processSuccessfulTask: (
|
||||
t: Task,
|
||||
a: { related_document: number },
|
||||
p: Task | null,
|
||||
) => Promise<void>;
|
||||
}
|
||||
).processSuccessfulTask(task, apiTask, parentTask);
|
||||
}
|
||||
|
||||
function lastPatchPayload(): PatchPayload {
|
||||
const calls = paperlessService.updateDocument.mock.calls as Array<
|
||||
[number, PatchPayload]
|
||||
>;
|
||||
return calls[calls.length - 1][1];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
paperlessService = {
|
||||
getDocument: jest.fn(),
|
||||
updateDocument: jest.fn().mockResolvedValue(undefined),
|
||||
addNote: jest.fn().mockResolvedValue(undefined),
|
||||
getDocumentMetadata: jest.fn().mockResolvedValue({
|
||||
original_checksum: 'abc',
|
||||
original_filename: 'test.pdf',
|
||||
}),
|
||||
getTask: jest.fn(),
|
||||
};
|
||||
taskRepo = { save: jest.fn().mockResolvedValue(undefined) };
|
||||
documentRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn((v: object) => v),
|
||||
save: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
attachmentRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
save: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
service = new PaperlessTaskProcessorService(
|
||||
taskRepo as any,
|
||||
documentRepo as any,
|
||||
attachmentRepo as any,
|
||||
paperlessService as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('baut den PATCH-Payload mit ASN, CF3/CF7/CF9, Typ, created, owner und Tag-Merge', async () => {
|
||||
paperlessService.getDocument.mockResolvedValue({
|
||||
id: 101,
|
||||
title: 'Testdokument',
|
||||
custom_fields: [{ field: 7, value: 'alt' }],
|
||||
tags: [99],
|
||||
});
|
||||
const task = createTask({
|
||||
InterneBelegnummer: '2026-000123',
|
||||
externeBelegnummer: 'RE-9',
|
||||
Eingangsdatum: new Date('2026-07-14T00:00:00Z'),
|
||||
Belegdatum: new Date('2026-07-01T00:00:00Z'),
|
||||
DocumentType: 2,
|
||||
BetriebID: 3,
|
||||
Tags: '1,2',
|
||||
CustomFieldsJson: JSON.stringify({ '4': 'x' }),
|
||||
});
|
||||
|
||||
await runProcessSuccessfulTask(task, { related_document: 101 }, null);
|
||||
|
||||
expect(paperlessService.updateDocument).toHaveBeenCalledTimes(1);
|
||||
expect(paperlessService.updateDocument).toHaveBeenCalledWith(
|
||||
101,
|
||||
expect.anything(),
|
||||
);
|
||||
const payload = lastPatchPayload();
|
||||
expect(payload.archive_serial_number).toBe(2026000123);
|
||||
expect(payload.document_type).toBe(2);
|
||||
expect(payload.created).toBe('2026-07-01T00:00:00.000Z');
|
||||
expect(payload.owner).toBe(3);
|
||||
expect(payload.tags).toEqual([99, 1, 2]);
|
||||
expect(sortByField(payload.custom_fields)).toEqual([
|
||||
{ field: 3, value: 'RE-9' },
|
||||
{ field: 4, value: 'x' },
|
||||
{ field: 7, value: '2026-000123' },
|
||||
{ field: 9, value: '2026-07-14' },
|
||||
]);
|
||||
expect(task.Fertig).toBe(1);
|
||||
expect(task.PaperlessDocumentID).toBe(101);
|
||||
});
|
||||
|
||||
it('entfernt den Owner (null) bei leerer BetriebID', async () => {
|
||||
paperlessService.getDocument.mockResolvedValue({
|
||||
id: 102,
|
||||
title: 'Ohne Betrieb',
|
||||
custom_fields: [],
|
||||
tags: [],
|
||||
});
|
||||
const task = createTask({ InterneBelegnummer: '2026-000124' });
|
||||
|
||||
await runProcessSuccessfulTask(task, { related_document: 102 }, null);
|
||||
|
||||
expect(lastPatchPayload().owner).toBeNull();
|
||||
});
|
||||
|
||||
it('überschreibt bei Anlagen document_type/title und verknüpft CF8 zum Elterndokument', async () => {
|
||||
paperlessService.getDocument
|
||||
.mockResolvedValueOnce({
|
||||
id: 103,
|
||||
title: 'Anlage-Roh',
|
||||
custom_fields: [],
|
||||
tags: [],
|
||||
})
|
||||
.mockResolvedValueOnce({ id: 55, title: 'Eltern' });
|
||||
paperlessService.getTask.mockResolvedValue([{ related_document: 55 }]);
|
||||
const task = createTask({
|
||||
DocumentType: 2,
|
||||
TaskReferenceID: 'parent-uuid',
|
||||
});
|
||||
const parentTask = createTask({
|
||||
TaskId: 'parent-uuid',
|
||||
InterneBelegnummer: '2026-000100',
|
||||
Fertig: 1,
|
||||
});
|
||||
|
||||
await runProcessSuccessfulTask(task, { related_document: 103 }, parentTask);
|
||||
|
||||
const payload = lastPatchPayload();
|
||||
expect(payload.document_type).toBe(5);
|
||||
expect(payload.title).toBe('Anlage zu 2026-000100');
|
||||
expect(sortByField(payload.custom_fields)).toEqual([
|
||||
{ field: 8, value: [55] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user