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>
202 lines
6.1 KiB
TypeScript
202 lines
6.1 KiB
TypeScript
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] },
|
|
]);
|
|
});
|
|
});
|